Showing detailed progress for running WorkManager workers - android

I want to replace the job scheduling aspect of my existing data syncing system with the new JetPack WorkManager (link to codelabs) component (in a sandbox branch of the app). My existing system works well but some of the new features in WorkManager would come in handy (e.g. chaining).
My current system uses a shared LiveData to communicate the progress from a job in progress to any UI element (RecyclerView in my case) observing on it (I'm actually SwitchMapping in the ViewModel into a list of SyncItems)
data class SyncItem(
val title: String,
private var _progress: Int,
var total: Int) : BaseObservable() {
var progress: Int
#Bindable get() = _progress
set(value) {
_progress = value
notifyPropertyChanged(BR.progress)
}
}
The new WorkManager component has several methods (getStatusById, getStatusesByTag, etc.) that can be used to retrieve a LiveData with one or more WorkStatuses, but these only report a course-grained status (running, success, failed, cancelled).
What is the recommended way of communicating progress (e.g. '546/1234 items downloaded') to the UI? The setOutputData/getOutputData pair seems to be used more to communicate between Workers (which I need when chaining) than with the UI.
Attached is a screenshot of what it looks like (in a [test] version of my app using my old method) when a user opens the sync status page (2 items completed, rest in progress).
In the final product the user will be able to cancel any jobs in progress and re-issue once-off work requests. Normally the jobs will be fired off by PeriodicWorkRequest.

Natively Supported
implementation 'androidx.work:work-runtime:2.5.0'
Report progress on Worker:
public class FooWorker extends Worker {
public FooWorker(#NonNull Context context, #NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
#NonNull
#Override
public Result doWork() {
try {
setProgressAsync(new Data.Builder().putInt("progress", 0).build());
Thread.sleep(1000);
setProgressAsync(new Data.Builder().putInt("progress", 50).build());
Thread.sleep(1000);
setProgressAsync(new Data.Builder().putInt("progress", 100).build());
return Result.success();
} catch (InterruptedException e) {
e.printStackTrace();
return Result.failure();
}
}
}
Observe progress of Worker:
WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("test").observe(lifecycleOwner, new Observer<List<WorkInfo>>() {
#Override
public void onChanged(List<WorkInfo> workInfos) {
if (workInfos.size() > 0) {
WorkInfo info = workInfos.get(0);
int progress = info.getProgress().getInt("progress", -1);
//Do something with progress variable
}
}
});
ListenableWorker now supports the setProgressAsync() API, which allows it to persist intermediate progress. These APIs allow developers to set intermediate progress that can be observed by the UI. Progress is represented by the Data type, which is a serializable container of properties (similar to input and output, and subject to the same restrictions).
See Android Documentation

The best way to do it is to write intermediate progress to your own data store and expose a LiveData<>. We are considering adding this feature in a future alpha.

Related

Why am I getting ConcurrentModificationException when call Wearable.getMessageClient

This looks like a concurrency bug inside Google Wearable API, but let me know if I'm wrong and suggest a way around. The purpose of this code is to get a Wearable message client and send a message to a WearOS device.
I need to do it asynchronously to avoid locking the main thread. It looks like Google's implementation of getMessageClient is not thread safe. Should I lock on some object to avoid the crashes? What would be the best approach here? Why Google couldn't add a callback in a thread safe manner?
IMO, all they needed to do was to lock the SimpleArrayMap object before "put" is called and release after "put" is completed.
fun sendToWear(msg:String) {
lifecycle.coroutineScope.launch {
withContext(Dispatchers.IO) {
val nodeListTask = Wearable.getNodeClient(applicationContext).getConnectedNodes()
try {
val nodes = Tasks.await<List<Node>>(nodeListTask)
Tasks.await<List<Node>>(nodeListTask, 10, TimeUnit.SECONDS)
for (node in nodes) {
//Send the message///
val sendMessageTask = Wearable.getMessageClient(this#RootActivity).sendMessage(node.getId(),
BROADCAST_PATH, msg.toByteArray())
Stack trace:
java.util.ConcurrentModificationException:
at androidx.collection.SimpleArrayMap.put (SimpleArrayMap.java:482)
at com.google.android.gms.common.api.internal.zzc.addCallback (zzc.java:20)
at com.google.android.gms.common.api.internal.zaae.<init> (zaae.java:14)
at com.google.android.gms.common.api.internal.zaae.zaa (zaae.java:5)
at com.google.android.gms.common.api.GoogleApi.<init> (GoogleApi.java:31)
at com.google.android.gms.wearable.MessageClient.<init> (MessageClient.java:3)
at com.google.android.gms.wearable.internal.zzez.<init> (zzez.java:4)
at com.google.android.gms.wearable.Wearable.getMessageClient (Wearable.java:11)
at info.gryb.gac.mobile.fragments.RootActivity$sendToWear$1$1.invokeSuspend (RootActivity.kt:1048)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run (Dispatched.kt:241)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely (CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$createdWorkers (CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely (CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run (CoroutineScheduler.kt:740)
It's crashing while trying to add a callback by calling "put" in Google API's code below:
public final void addCallback(String var1, #NonNull LifecycleCallback var2) {
if (!this.zzbe.containsKey(var1)) {
this.zzbe.put(var1, var2); <-- Crashes in this put
if (this.zzbf > 0) {
(new zze(Looper.getMainLooper())).post(new zzd(this, var2, var1));
}
} else {
throw new IllegalArgumentException((new StringBuilder(59 + String.valueOf(var1).length())).append("LifecycleCallback with tag ").append(var1).append(" already added to this fragment.").toString());
}
}
It's definitely worth raising as a bug, either for a concurrency fix, or alternatively clearer documentation.
But the MessageClient apis are all async anyway, they return Task that indicate the result when available, so it doesn't seem like you should need to run these on the IO Dispatcher.
You should also be able to use org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2 to have kotlin friendly await methods.
val nodeList = Wearable.getNodeClient(applicationContext).getConnectedNodes().await()
So

How to make network API calls on SliceProvider and set the information on Slice

I have been struggling on this question a few days. So what I want to do on Android Slices is create slice with information which is from the back-end service. For example:
on SliceProvider class
#Override
public Slice onBindSlice(Uri sliceUri) {
l.d(TAG,"onBindSlice called");
switch (sliceUri.getPath()) {
case "/product":
makeRequestForProduct();
return createProductSlice(sliceUri);
}
return null;
}
and
private void makeRequestForProduct() {
String url = Environment.getInstance().getCompleteUrl("etc..");
RetrofitFactory.defaultBuilder(ProductWebInterface.class)
.getProductResponse(url).enqueue(new ProductWebcallback());
}
public void onEventMainThread(ProductReceivedEvent response) {
if (response.getProduct() != null) { //do something
}
}
But I have no idea how to do it. Above code is not working. It is giving me an Exception.
According to Google Documentation here :
onBindSlice should return as quickly as possible so that the UI tied to this slice can be responsive. No network or other IO will be allowed during onBindSlice. Any loading that needs to be done should happen in the background with a call to ContentResolver.notifyChange(Uri, ContentObserver) when the app is ready to provide the complete data in onBindSlice.
You must therefore do your work in the background thread.
See an example below in Kotlin:
private fun makeRequestForProductInTheBackground(sliceUri : SliceUri) {
Completable.fromAction {
makeRequestForProduct(sliceUri)
}.subscribeOn(Schedulers.io()).subscribe()
}
After the request completes you can save your data somewhere e.g a variable or a repository.
fun onEventMainThread(response: ProductReceivedEvent) {
if (response.getProduct() != null) {
//Save your data in a variable or something depending on your needs
product == response.getProduct()
//This will call the *onBindSlice()* method again
context?.contentResolver?.notifyChange(sliceUri, null)
}
}
You can then use the product data in your createProductSlice(sliceUri) method

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.

Is it possible to send a synchronous request in the Firebase?

I'm using Firebase Android SDK and became interested in sending synchronous request instead of asynchronous. According to the documentation, in any request callbacks are presented. But what about the synchronicity?
Thanks!
There is no way to synchronously load data from the Firebase Database.
While it is common for developers new to Firebase to wish for a synchronous method, it simply doesn't fit with Firebase's data synchronization model. Also see my answer here: Setting Singleton property value in Firebase Listener
It is not possible to load data synchronously with the official SDK. However, you can access all the data in firebase using the REST API. This would allow you to make synchronous calls. As mentioned about, Firebase is a realtime database and you will be missing the feature of updates when your data changes.
I made a simple class to call tasks synchronously in Android.
Note that this is similar to Javascript's async await function.
Check my gist.
TasksManager.class
public class TasksManager {
...
public ExecutorService getExecutor() {
if (mDefaultExecutor == null || mDefaultExecutor.isShutdown() || mDefaultExecutor.isTerminated()) {
// Create a new ThreadPoolExecutor with 2 threads for each processor on the
// device and a 60 second keep-alive time.
int numCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
numCores * 2,
numCores * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
mDefaultExecutor = executor;
}
return mDefaultExecutor;
}
public static <TResult> Task<TResult> call(#NonNull Callable<TResult> callable) {
return Tasks.call(getInstance().getExecutor(), callable);
}
}
Here's a sample code to use it.
TasksManager.call(() -> {
Tasks.await(AuthManager.signInAnonymously());
// You can use multiple Tasks.await method here.
// Tasks.await(getUserTask());
// Tasks.await(getProfileTask());
// Tasks.await(moreAwesomeTask());
// ...
startMainActivity();
return null;
}).addOnFailureListener(e -> {
Log.w(TAG, "signInAnonymously:ERROR", e);
});
While it is not possible to load data from the FirebaseDatabase in a synchronous way, it is possible to wait for the load to finish synchronously.
You can wrap your value listener in a CountDownLatch and count down,
once the onDataChange or onCancelled implementation is called.
This is actually what the Tasks api is doing internally if you call Tasks.await(someTask).
You should use the value listener for single event listening, because in this case I assume you don't want continued updates. And use a proper timeout for the CountDownLatch, since Firebase won't timeout, ever.
reference.addListenerForSingleValueEvent(...);
You also have to take into account, that if you have the FirebaseDatabase
cache enabled, the first result might not be the actual value on the
server.
I have to add: While this might work, it is against the idea how firebase is designed and supposed to be used, as Frank already said.
If you are using Kotlin, add an extension function:
private suspend fun <TResult> Task<TResult>.await(): TResult? {
return try {
Tasks.await(this)
} catch (e: Exception) {
null
}
}
Now you can do
val snapshot = fireStore.collection(USER_ROOT_PATH).document(documentPath)?.get()?.await()

Categories

Resources