Change the period of a periodic task in Firebase JobDispatcher? - android

I was using GCM network manager, but then I heard that Firebase JobDispatcher includes GCM plus other features so I'm trying to use that.
I have successfully programmed a periodic task and it works fine, but the problem is that I need the period to change and not be fixed from the beginning.
The reason for that is, I'm using an activity recognition service and I want the next time the JobDispatcher executes the periodic task to be based on the detected current activity. For example if you're walking, the next time the task is triggered is after 30 minutes, while if you're in a car then the period is 5 minutes (mainly because if you're in a car it's more likely that your phone will provide different location values in a short while compared to when you're on foot).
This is the way I program the periodic task, as you can see I'm setting a fixed value, I want to know if the service that is triggered by this task can provide a feedback that'll change the period of the task.
final Builder builder = jobDispatcher.newJobBuilder()
.setTag(form.tag.get())
.setRecurring(form.recurring.get())
.setLifetime(form.persistent.get() ? Lifetime.FOREVER : Lifetime.UNTIL_NEXT_BOOT)
.setService(DemoJobService.class)
.setTrigger(Trigger.executionWindow(
form.getWinStartSeconds(), form.getWinEndSeconds()))
.setReplaceCurrent(form.replaceCurrent.get())
.setRetryStrategy(jobDispatcher.newRetryStrategy(
form.retryStrategy.get(),
form.getInitialBackoffSeconds(),
form.getMaximumBackoffSeconds()));
if (form.constrainDeviceCharging.get()) {
builder.addConstraint(Constraint.DEVICE_CHARGING);
}
if (form.constrainOnAnyNetwork.get()) {
builder.addConstraint(Constraint.ON_ANY_NETWORK);
}
if (form.constrainOnUnmeteredNetwork.get()) {
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
}

To do this you'd need to have your onStartJob in DemoJobService.class cancel itself using your tag that you've saved in SharedPrefs / elsewhere (form.tag.get() in your example):
FirebaseJobDispatcher(GooglePlayDriver(context)).cancel(yourTag)
It can then reschedule the job with the updated parameters. Unfortunately there's currently no way to edit jobs, they must be cancelled and recreated!

Related

Android WorkManager doesn't trigger one of the two scheduled workers

I have two periodic workers scheduled in my app where one worker repeats after 24 hours and another in 15 minutes.
Initially on fresh install things work as expected, but after some days I got an issue on 2 devices(out of 5).
The 24 hour worker is triggered properly but the 15 minute one isn't triggered at all. I have been monitoring this for 24 hours now.
I viewed the databse of workmanager via Stetho and saw some entries for 24-hour worker and 0 entries for 15 minute worker. I'm looking in the WorkSpec table.
I debugged via Android studio and after querying WorkManager using getWorkInfosByTag() I got a list of 80 objects for the 15-minute worker where 79 were in CANCELED state and one was in ENQUEUED state.
So apparently, canceled workers are not added to the DB?
I did not find any document from Google which explains the scenarios in which worker is canceled.
I am using 1.0.0-beta03 version of the work runtime.
Also, I am not killing the app or doing anything funny. The app is running in the background and not being killed.
Devices are Mi A2 (Android 9), Redmi Note 4(Android 7).
I need to understand why is the worker being canceled and is there any better way to debug this? Any pointers will be helpful and upvoted!
Thanks.
Edit1: Posting the code to schedule both workers.
24-hour periodic worker:
public static synchronized void scheduleWork() {
checkPreviousWorkerStatus();
if (isWorking()) {
Log.i("AppDataCleanupWorker", "Did not schedule data cleanup work; already running.");
return;
}
if (lastWorkId != null) {
WorkManager.getInstance().cancelAllWorkByTag("AppDataCleanupWorker");
lastWorkId = null;
}
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(true)
.build();
PeriodicWorkRequest.Builder builder = new PeriodicWorkRequest
.Builder(AppDataCleanupWorker.class, 24, TimeUnit.HOURS)
.addTag("AppDataCleanupWorker")
.setConstraints(constraints);
PeriodicWorkRequest workRequest = builder.build();
lastWorkId = workRequest.getId();
WorkManager.getInstance().enqueue(workRequest);
List<WorkInfo> workInfos = WorkManager.getInstance()
.getWorkInfosByTagLiveData("AppDataCleanupWorker")
.getValue();
if (workInfos != null && workInfos.size() > 1) {
throw new RuntimeException("Multiple workers scheduled. Only one schedule is expected.");
}
}
15-minute periodic worker:
public static synchronized void scheduleWork() {
checkPreviousWorkerStatus();
if (isWorking) {
Log.i("ImageUploadWorker", "Did not schedule image upload work; already running.");
return;
}
if (lastWorkId != null) {
WorkManager.getInstance().cancelAllWorkByTag("ImageUploadWorker");
lastWorkId = null;
}
Constraints constraints = new Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest.Builder builder =
new PeriodicWorkRequest.Builder(ImageUploadWorker.class, 15,
TimeUnit.MINUTES)
.addTag("ImageUploadWorker")
.setConstraints(constraints);
PeriodicWorkRequest workRequest = builder.build();
lastWorkId = workRequest.getId();
WorkManager.getInstance().enqueue(workRequest);
List<WorkInfo> workInfos = WorkManager.getInstance()
.getWorkInfosByTagLiveData("ImageUploadWorker").getValue();
if (workInfos != null && workInfos.size() > 1) {
throw new RuntimeException("Multiple workers scheduled. Only one schedule is expected.");
}
}
Note: The device is connected to the Internet & network speed is pretty good.
SOLVED: Worker not being triggered by WorkManager
Resolved the issue after some debugging. Posting here in case someone runs into the same issue.
So, I was canceling and enqueuing workers again and again. So lets say a worker is scheduled for 11.15 AM today, then I cancel and enqueue again, the 11.15 AM slot was not being given to the newly enqueued worker.
Instead, When the 11.15 AM slot is utilised, the work manager just checks that the scheduled worker was canceled and does not trigger the newly enqueued worker.
This was the behaviour on 3 out of 5 devices we tested on. On 2 devices the newly enqueued worker was properly being triggered.
Now the solution:
Remove all code to schedule your workers.
In the onCreate() of your application, first invoke pruneWork() on WorkManager to remove all piled up cancelled worker schedules. Remember the method returns Operation which will help you check the completion of removal.
Before calling pruneWork() you might also call cancelAllWorkByTag() for all your workers to clean up any and all the pending schedules. This method also returns an Operation.
After the work manager schedules are cleared, you can now schedule your PeriodicWorkRequest the way you want. I used enqueueUniquePeriodicWork() to make sure only one instance of worker is running at a time.
Now, my worker is being triggered every 15 minutes properly.
Note that as and when your device sleeps and goes into doze mode, this 15 minute duration will increase.
You can check the work manager database using Stetho library.
The table name is WorkSpec where you'll find all the schedules for your workers. And you can stop app execution at some breakpoint and use getWorkInfosByTag() on WorkManager to get a list of schedules and their current status.
You're doing a few things that are incorrect.
You're using LiveData and calling getValue() on it without adding an Observer. This won't give you what you're looking for - the LiveData never starts tracking the values that you want. Please check out proper LiveData usage here: https://developer.android.com/topic/libraries/architecture/livedata
If you only want one particular copy of a type of work, you should use enqueueUniqueWork instead of enqueue.
Unless you found yourself in an extremely bad situation where you actually need to remove old workers, I would advise you not to call pruneWork(). Please see the documentation: https://developer.android.com/reference/androidx/work/WorkManager#pruneWork()

Periodic Work manager not showing notification on android pie when not charging or screen off

I have an app that should show a notification every 2 hours and should stop if user has already acted upon the notif. Since background services are history now, I thought of using WorkManager ("android.arch.work:work-runtime:1.0.0-beta01") for the same.
My problem is that although the work manager is successfully showing the notifications when app is running, but it won't show notification consistently in the following cases(I reduced the time span from 2 hours to 2 minutes to check the consistency):
when app is killed from the background.
device is in screen off.
state device is in unplugged state(i.e not charging).
By consistency , i mean that the notifications show at least once in the given time span. for 2 minutes time span, the freq of notifications went from once every 4 minutes to completely not show any notification at all. for 2 hours timespan( the timespan that i actually want), its been 4 hours and i haven't got a single notification. Here is the Code i am using for calling WorkManger:
public class CurrentStreakActivity extends AppCompatActivity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
setDailyNotifier();
...
}
private void setDailyNotifier() {
Constraints.Builder constraintsBuilder = new Constraints.Builder();
constraintsBuilder.setRequiresBatteryNotLow(false);
constraintsBuilder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
constraintsBuilder.setRequiresCharging(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
constraintsBuilder.setRequiresDeviceIdle(false);
}
Constraints constraints =constraintsBuilder.build();
PeriodicWorkRequest.Builder builder = new PeriodicWorkRequest
.Builder(PeriodicNotifyWorker.class, 2, TimeUnit.HOURS);
builder.setConstraints(constraints);
WorkRequest request = builder.build();
WorkManager.getInstance().enqueue(request);
}
....
}
Here is the worker class(i can post showNotif(..) and setNotificationChannel(...) too if they might be erroronous):
public class PeriodicNotifyWorker extends Worker {
private static final String TAG = "PeriodicNotifyWorker";
public PeriodicNotifyWorker(#NonNull Context context, #NonNull WorkerParameters workerParams) {
super(context, workerParams);
Log.e(TAG, "PeriodicNotifyWorker: constructor called" );
}
#NonNull
#Override
public Result doWork() {
// Log.e(TAG, "doWork: called" );
SharedPreferences sp =
getApplicationContext().getSharedPreferences(Statics.SP_FILENAME, Context.MODE_PRIVATE);
String lastcheckin = sp.getString(Statics.LAST_CHECKIN_DATE_str, Statics.getToday());
// Log.e(TAG, "doWork: checking shared preferences for last checkin:"+lastcheckin );
if (Statics.compareDateStrings(lastcheckin, Statics.getToday()) == -1) {
Log.e(TAG, "doWork: last checkin is smaller than today's date, so calling creating notification" );
return createNotificationWithButtons(sp);
}
else {
Log.e(TAG, "doWork: last checkin is bigger than today's date, so no need for notif" );
return Result.success();
}
}
private Result createNotificationWithButtons(SharedPreferences sp) {
NotificationManager manager =
(NotificationManager) getApplicationContext().getSystemService((NOTIFICATION_SERVICE));
String channel_ID = "100DaysOfCode_ID";
if (manager != null) {
setNotificationChannel(manager,channel_ID);
showNotif(manager, channel_ID, sp);
return Result.success();
}
else {
return Result.failure();
}
I am using a xiaomi miA2 androidOne device with Android Pie(SDK 28). There are a few other things that are troubling me:
What can i possibly do to know if my WorkManager is running? Other that just wait for 2 hours and hope for a notification. I actually tried something like that, keeping my phone connected to pc and checking android studio's logcat every now and then. It DOES run all the logs when the worker is actually called, but i don't think that's a correct way to test it, or is it?
In the above Code, the setDailyNotifier() is called from the onCreate() every time the app is opened. Isn't it Wrong? shouldn't there be some unique id for every WorkRequest and a check function like WorkManger.isRequestRunning(request.getID) which could let us check if a worker is already on the given task??If this was a case of AsyncTask, then boy we would have a mess.
I have also checked #commonsware's answer here about wakelock when screen is off, but i remember that work manager does use alarm manager in the inside when available. So what am I missing here?
Few comments:
WorkManager has a minimum periodic interval of 15minutes and does not guarantee to execute your task at a precise time. You can read more about this on this blog.
All the usual background limitation you've on newer Android releases are still relevant when you use WorkManager to schedule your tasks. WorkManager guarantees that the task are executed even if the app is killed or the device is restated, but it cannot guarantee the exact execution.
There's one note about the tasks being rescheduled when your app is killed. Some OEM have done modification to the OS and the Launcher app that prevents WorkManager to be able to accomplish these functionality.
Here's the issuetracker discussion:
Yes, it's true even when the phone is a Chinese phone.
The only issue that we have come across is the case where some Chinese OEMs treat swipe to dismiss from Recents as a force stop. When that happens, WorkManager will reschedule all pending jobs, next time the app starts up. Given that this is a CDD violation, there is not much more that WorkManager can do given its a client library.
To add to this, if a device manufacturer has decided to modify stock Android to force-stop the app, WorkManager will stop working (as will JobScheduler, alarms, broadcast receivers, etc.). There is no way to work around this. Some device manufacturers do this, unfortunately, so in those cases WorkManager will stop working until the next time the app is launched.
As of now , i have this app installed for last 8 days and i can confirm that the code is correct and app is working fine. as said by pfmaggi , the minimum time interval for work manager to schedule the work is 15 minutes, so there is a less chance that the WorkManager would have worked as expected in my testing conditions( of 2 minutes ) . Here are some of my other observations:
Like I said in the question that i was unable to recieve a notification for 4 hours even though i have passed the repeat interval as 2 hours. This was because of Flex Time. I passed in the flex time of 15 minutes and now it shows notifications between correct time interval. so i will be marking pfmaggi's answer as correct.
The problem of repeated work request can be solved by replacing WorkManager.getInstance().enqueue(request) with WorkManager.getInstance().enqueueUniqueWork(request,..)
I was still unable to find a way to test the work manager in the way i have described.

Is it possible to re-schedule job within your job

I'm using Evernote's JobScheduler. The library provides a way to set a periodic job, however that has a minimum interval of 15 minutes between repetitions.
I want to be able to send a user's location to the server every few minutes (maximum 3, minimum 2). Is it technically OK to schedule your old job again and again after the job is done? Something like this:
protected Result onRunJob(Params params)
{
// my actual job runs here
schedulePeriodicJob();
return Result.SUCCESS;
}
private void schedulePeriodicJob()
{
jobId = new JobRequest.Builder(ScheduleJob.TAG)
.setExact(TimeUnit.MINUTES.toMillis(2))
.setBackoffCriteria(TimeUnit.MINUTES.toMillis(1), JobRequest.BackoffPolicy.LINEAR)
.build()
.schedule();
}
Or should I simply use a foreground service to achieve this?
Evernote job scheduler is based upon JobScheduler, GcmNetworkManager or AlarmManager depending on the context/device/playservices version. Whatever the implementation, APIs exposes the following method:
public JobRequest.Builder setPeriodic(long intervalMs, long flexMs)
I would use this one instead of "manually" rescheduling job. I also don't think your setBackoffCriteria call would not work without setting recurring job at APIs level.

Schedule recurring job using firebase job dispatcher

I am trying to post the location of the android device to server every 10 minutes. I am using firebase job dispatcher to do this
FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(this));
Job myJob = dispatcher.newJobBuilder()
.setService(UpdateLocationService.class)
.setRecurring(true)
.setTrigger(Trigger.executionWindow(10, 20))
.setRetryStrategy(RetryStrategy.DEFAULT_LINEAR)
.setTag("location-update-job")
.setLifetime(Lifetime.FOREVER)
.build();
dispatcher.mustSchedule(myJob);
UpdateLocationService gets the location and sends to server.
My problem: Things are mostly working fine. Only thing is, the jobs are getting scheduled with a difference of 4m, 6m, 7m, 8m, 10m, 16m, 23m...
Can some one please help me understand going on.
Update: I want the location once in 10-20 minutes. In the above code, the value is too low just for the testing purposes
Also the:
Trigger.executionWindow(windowStart, windowEnd)
expects the windowStart and windowEnd in seconds. As per your requirement, you want the window to be 10 mins. So you should use something like:
Trigger.executionWindow(10*60, 20*60)
There are a few reasons why this could be happening. Firstly is your job returning false in onStopJob()? From the docs
#Override
public boolean onStopJob(JobParameters job) {
return false; // Answers the question: "Should this job be retried?"
}
If the job needs be retried then the backoff will be applied. Combine this with the fact you want it to run again every 10-20 seconds you might get the results you are experiencing.
You have not set any constraints for the job, which also will affect when it will run. e.g.
.setConstraints(
// only run on an unmetered network
Constraint.ON_UNMETERED_NETWORK,
// only run when the device is charging
Constraint.DEVICE_CHARGING
)
Furthermore, I would not use a scheduled job for what you are doing. Look at the Google API Client which offers periodic updates from the fused location provider.
You can implement a callback on your Service or Activity like so
public class MainActivity extends ActionBarActivity implements
ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
...
#Override
public void onLocationChanged(Location location) {
mCurrentLocation = location;
mLastUpdateTime = DateFormat.getTimeInstance().format(new Date());
updateUI();
}
private void updateUI() {
mLatitudeTextView.setText(String.valueOf(mCurrentLocation.getLatitude()));
mLongitudeTextView.setText(String.valueOf(mCurrentLocation.getLongitude()));
mLastUpdateTimeTextView.setText(mLastUpdateTime);
}
}
Checkout the full docs here but I believe you will have a more consistent experience with services dedicated to what you are trying to achieve.
https://developer.android.com/training/location/receive-location-updates.html
ExecutionWindow specifies approximate time. It's not guaranteed that job will run at the given window. If it misses the window the job will run at earliest time later under ideal circumstances.For recurring jobs once the job has finished next job will calculate execution window time from the time job last run.
LINK
ExecutionWindow represents a Job trigger that becomes eligible once
the current elapsed time exceeds the scheduled time + the {#code windowStart}
value. The scheduler backend is encouraged to use the windowEnd value as a
signal that the job should be run, but this is not an enforced behavior.

Flex mobile: remove GeolocationEvent.UPDATE does not work

I can't seem to remove the listener for the update event for geolocation on Android.
I wanna stop the Geolocation on deactivate:
NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, onAppDeactivate);
private function onAppDeactivate(e:Event):void {
if (Geolocation.isSupported) {
if (geolocation != null) {
geolocation.removeEventListener(GeolocationEvent.UPDATE, onGeolocationUpdate);
geolocation.setRequestedUpdateInterval(0);
geolocation = null;
}
}
}
I started with just removing the listener, but since that didn't work I also tried removing the geolocation all together. Still no luck..
Any hints?
I had an app that uses Geolocation too, and if what you want is to save the battery from draining fast, you have to call the geolocation.setRequestedUpdateInterval(INTERVAL_MILLIS) method with a high value for INTERVAL_MILLIS (in my case i use 60000 that is 1 min).
But as the (documentation) says, the OS will decide the update interval for the GPS, ant the value we pass serves a a "hint" to the update interval.
And specifically on Android, the GPS icon will stay on as long as you have the GPS enabled (and it will consume power) but will no drain you battery fast as the update interval will be high.
So the best thing you can do is actually request a large update interval when you app deactivates.

Categories

Resources