When to call JobScheduler.schedule() - android

What I want to implement is a background service that fetch data from the server every hour. I expect this service can run periodically after the boot. Thus, I choose to use JobScheduler to implement this function.
val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val builder = JobInfo.Builder(1, ComponentName(this, PullDataJob::class.java))
.setPeriodic(1000L * 60 * 60) // one hour
.setRequiredNetworkType(NETWORK_TYPE_ANY)
.setPersisted(true)
jobScheduler.schedule(builder.build())
This is my currently code, which is placed inside onCreate(). However, I find that if I put the JobScheduler.schedule() in onCreate(), the service will be automatically executed every time I open the application.
Where is the best place to put the code above to make the service run periodically even if the user never opens the application after system boot?

Hakeem is right, you should only schedule it once.
In case you schedule a job with the same JobId twice, the documentation states following:
Will replace any currently scheduled job with the same ID with the new information in the JobInfo. If a job with the given ID is currently running, it will be stopped.
But i would solve the problem different to the way hakeem did. Instead of saving this information in a Sharedpreference you should use the JobScheduler to determine if the job with your id has been scheduled already. This way you are more robust and will reschedule the job in case some weird thing happened and your job is not scheduled anymore.
Code:
public static boolean isJobServiceOn( Context context ) {
JobScheduler scheduler = (JobScheduler) context.getSystemService( Context.JOB_SCHEDULER_SERVICE ) ;
boolean hasBeenScheduled = false ;
for ( JobInfo jobInfo : scheduler.getAllPendingJobs() ) {
if ( jobInfo.getId() == RETRIEVE_DATA_JOB_ID ) {
hasBeenScheduled = true ;
break ;
}
}
return hasBeenScheduled ;
}
When scheduling the Job you can then just use this function to determine if the job i currently scheduled or not.

Your job is executed periodically (once every hour), so once it's run the first time, JobScheduler.schedule() should never be called again.
Accomplishing this is quite easy, once you call JobScheduler.schedule() for the first time, register the fact that it's been scheduled, and only run the scheduling method when you're sure your job have never been run before.
public static final String IS_JOB_FIRST_RUN = "job scheduled";
...
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
if (preferences.getBoolean(IS_JOB_FIRST_RUN, true)) {
// your code
JobScheduler.schedule();
preferences.edit().putBoolean(IS_JOB_FIRST_RUN, false).apply();
}

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.

How to change periodic work request period without it running immediately using WorkManager?

val request = PeriodicWorkRequestBuilder<FooWorker>(1, TimeUnit.DAYS).build()
WorkManager.getInstance().enqueueUniquePeriodicWork(
"FOO",
ExistingPeriodicWorkPolicy.REPLACE,
request
)
The code above is ran in onCreate of Application to ensure the request is enqueued. However, this will cause a problem where the FooWorker will run every time user start the application because ExistingPeriodicWorkPolicy.REPLACE cancel previous work and enqueue new work which cause it run immediately.
If we change to ExistingPeriodicWorkPolicy.KEEP, we will not able to replace the work even the period or the worker is changed.
Is there a way to replace the request after the current one is running?
For example, origin request run a 1 time per day and the new request is 1 time per hour. After the next origin request is ran, then replace it with new request.
There is no way to do exactly what you want with periodic work in a clean way.
However, there's absolutely no need to use periodic work itself. The same structure can be easily accomplished by scheduling the next WorkRequest at the end of your doWork method, right before returning Result.SUCCESS:
fun doWork(): Result {
reallyDoWork()
// Now schedule the next "periodic" work
val request = OneTimeWorkRequestBuilder<FooWorker>().build()
WorkManager.getInstance().enqueueUniqueWork(
"FOO",
ExistingWorkPolicy.REPLACE,
request
)
return Result.SUCCESS
}
With this setup, your onCreate() of Application can safely use ExistingWorkPolicy.KEEP to avoid rescheduling work if you already have a WorkRequest queued up and when that queued up work fires, the next WorkRequest will be queued up with the appropriate new period.
Looks like there is a way to replace a periodic work now with enqueueUniquePeriodicWork.
val request = PeriodicWorkRequest.Builder(FooWorker::class.java, 1, TimeUnit.DAYS).build()
WorkManager.getInstance(appContext)
.enqueueUniquePeriodicWork(WORK_TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
Make sure you are passing ExistingPeriodicWorkPolicy instead of ExistingWorkPolicy
If you want to use ExistingPeriodicWorkPolicy.KEEP and to update PeriodicWorkRequest only when you want to change repeat interval, there is no solution from WorkManager, but you can save interval in sharedPreferences and check if interval was changed. And based on that to use ExistingPeriodicWorkPolicy.KEEP or ExistingPeriodicWorkPolicy.REPLACE
public static void enqueue(Context context) {
Log.d(TAG, "enqueue()");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
int repeatInterval = sharedPreferences.getInt("REPEAT_INTERVAL_HOURS", 0);
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest
.Builder(FooWorker.class, REPEAT_INTERVAL_HOURS, TimeUnit.HOURS)
.build();
ExistingPeriodicWorkPolicy policy = repeatInterval == REPEAT_INTERVAL_HOURS ? ExistingPeriodicWorkPolicy.KEEP : ExistingPeriodicWorkPolicy.REPLACE;
sharedPreferences.edit().putInt("REPEAT_INTERVAL_HOURS", REPEAT_INTERVAL_HOURS).apply();
WorkManager.getInstance(context).enqueueUniquePeriodicWork("fileRemove", policy, periodicWorkRequest);
}

Schedule a job in android to run at a specific time

I have an Android application which needs to send backup data every midnight to my cloud server. For this I need to schedule the job to occur every 24 hours without fail. I have read about AlarmManager and JobScheduler class which can schedule your jobs effectively.
In my case I am using JobScheduler class and here is my scheduleJob() method which sets a job to schedule after every 86400000 milliseconds (ie. one day).
#RequiresApi(api =Build.VERSION_CODES.LOLLIPOP)
public void scheduleJob(View v){
JobInfo.Builder builder = new JobInfo.Builder(1,new ComponentName(this,JobSchedule.class));
builder.setPersisted(true);
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
builder.setPeriodic(86400000);
builder.setRequiresCharging(false);
builder.setRequiresDeviceIdle(false);
builder.setBackoffCriteria(10000,JobInfo.BACKOFF_POLICY_EXPONENTIAL);
int test = jobScheduler.schedule(builder.build());
if(test <= 0){
//sth went wrong
}
}
My question stands is, from where and how should I implement jobSchedule() so that I can get the script to run exactly at 12 midnight.
Also, if I call scheduleJob method from, say, Android's Activity XYZ's onCreate() method, then everytime the Activity XYZ is called, it's override method onCreate() will be called and an additional job will be scheduled. So what should I do and from where should I schedule the job such that only one job is scheduled for the entire lifetime of the application?
To summarize using an example, Whatsapp backs up its data every day at 2 am. Even if I have turned off my net connection, it backs up my data as soon as I come online. I want to implement similar functionality in my application.
You can use SharedPreference to implement one-time execution. Something like this
SharedPreferences wmbPreference = PreferenceManager.getDefaultSharedPreferences(this);
boolean isFirstRun = wmbPreference.getBoolean("FIRSTRUN", true);
if (isFirstRun)
{
// Code to run once
SharedPreferences.Editor editor = wmbPreference.edit();
editor.putBoolean("FIRSTRUN", false);
editor.commit();
}
According to the documentation JobInfo Builder, setRequieresCharging and setRequieresDeviceIdle are set false by default, so you dont need to set them up false.

job scheduler in android N with less then 15 minutes interval

Part of my question, how I can set up a job with less then 15 minutes interval in "Nougat", was answerd by "blizzard" in his answer here:
Job Scheduler not running on Android N
He explained the problem and suggested to use the following workaround:
JobInfo jobInfo;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
jobInfo = new JobInfo.Builder(JOB_ID, serviceName)
.setMinimumLatency(REFRESH_INTERVAL)
.setExtras(bundle).build();
} else {
jobInfo = new JobInfo.Builder(JOB_ID, serviceName)
.setPeriodic(REFRESH_INTERVAL)
.setExtras(bundle).build();
}
However, using the suggested
.setMinimumLatency(REFRESH_INTERVAL)
just starts the job once;
but how do I get it periodic with a period of around 30 seconds on an android nougat device (not using handler or alarm manager)?
If someone is still trying to overcome the situation,
Here is a workaround for >= Android N (If you want to set the periodic job lower than 15 minutes)
Check that only setMinimumLatency is used. Also, If you are running a task that takes a long time, the next job will be scheduled at, Current JOB Finish time + PROVIDED_TIME_INTERVAL
.SetPeriodic(long millis) works well for API Level below Android N
#Override
public boolean onStartJob(final JobParameters jobParameters) {
Log.d(TAG,"Running service now..");
//Small or Long Running task with callback
//Reschedule the Service before calling job finished
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
scheduleRefresh();
//Call Job Finished
jobFinished(jobParameters, false );
return true;
}
#Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
private void scheduleRefresh() {
JobScheduler mJobScheduler = (JobScheduler)getApplicationContext()
.getSystemService(JOB_SCHEDULER_SERVICE);
JobInfo.Builder mJobBuilder =
new JobInfo.Builder(YOUR_JOB_ID,
new ComponentName(getPackageName(),
GetSessionService.class.getName()));
/* For Android N and Upper Versions */
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mJobBuilder
.setMinimumLatency(60*1000) //YOUR_TIME_INTERVAL
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
}
UPDATE:
If you are considering your repeating job to run while in Doze Mode and thinking about JobScheduler, FYI: JobSchedulers are not allowed to run in Doze mode.
I have not discussed about the Dozing because we were talking about JobScheduler. Thanks, #Elletlar, for pointing out that some may think that it will run even when the app is in doze mode which is not the case.
For doze mode, AlarmManager still gives the best solution. You can use setExactAndAllowWhileIdle() if you want to run your periodic job at exact time period or use setAndAllowWhileIdle() if you're flexible.
You can also user setAlarmClock() as device always comes out from doze mode for alarm clock and returns to doze mode again. Another way is to use FCM.
Reference: Doze Restrictions
https://developer.android.com/training/monitoring-device-state/doze-standby
I struggled with same thing when I wanted to setup Job for refresh small part of data. I found out that solution for this problem may be setting up Job one more time with the same ID after i calledjobFinished(JobParameters, boolean). I think it should work every time on main thread.
My function to setup Job looks like this:
JobInfo generateRefreshTokenJobInfo(long periodTime){
JobInfo.Builder jobBuilder = new JobInfo.Builder(1L, new ComponentName(mContext, JobService.class));
jobBuilder.setMinimumLatency(periodTime);
jobBuilder.setOverrideDeadline((long)(periodTime * 1.05));
jobBuilder.setRequiresDeviceIdle(false);
return jobBuilder.build();
}
When I finish my work after first call of Job i call in main thread
jobFinished(mJobParameters, true);
registerRefreshJob(5*60*1000L);
This will reschedule my Job one more time for the same amount of time on the same id. When device is in idle state you still have to take under consideration lack of wake locks in doze so your job may not be started so often as you wish. It is mentioned in https://developer.android.com/about/versions/nougat/android-7.0-changes.html
If the device is stationary for a certain time after entering Doze, the system applies the rest of the Doze restrictions to PowerManager.WakeLock, AlarmManager alarms, GPS, and Wi-Fi scans. Regardless of whether some or all Doze restrictions are being applied, the system wakes the device for brief maintenance windows, during which applications are allowed network access and can execute any deferred jobs/syncs.

Categories

Resources