54

I'm using JobScheduler to schedule jobs. Mainly I'm using it for the .setRequiredNetworkType() method, which allows you to specify that you only want the job to be scheduled when a network connection (or more specifically an unmetered connection) is established.

I'm using the following pretty straightforward code to schedule my jobs:

PersistableBundle extras = new PersistableBundle();
extras.putInt("anExtraInt", someInt);
int networkConstraint = useUnmetered ? JobInfo.NETWORK_TYPE_UNMETERED : JobInfo.NETWORK_TYPE_ANY;

ComponentName componentName = new ComponentName(context, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(jobId, componentName)
        .setRequiredNetworkType(networkConstraint)
        .setExtras(extras)
        .build();

JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo);

So there is just one constraint placed on the scheduling: a network connection (which may be 'any' or 'unmetered').

Short version of the question

How do I specify a maximum delay from all constraints being met, and actually running the job, e.g. "run the job within 2 seconds of there being a network connection"?

Longer version (with ramblings)

The Problem

What I'm finding is that on some devices, if a job is scheduled during a period in which the network constraint is already satisfied, the job will run immediately (or quickly enough to be perceived as so by the user).

But on other devices, even if a suitable network connection is already available (so that the job could run immediately), there is a significant delay before it actually runs. So if this is in response to a user action, the impression is that nothing has happened and that the app is broken.

Now, I'm well aware that this is probably the intention with JobScheduler... that it's up to the system to schedule the job to best fit in with other demands, and that there is no guarantee that the job will run immediately when all constraints are satisfied.

But it would be nice to be able to have some control over it, where required. So for jobs that happen on a schedule, without user involvement, giving the system complete control over precise timing is fine and good.

But where the job is in response to a user action, I want the job to run without delay... assuming the network connection is there. (If there is no connection, a message can be displayed that the action will happen when a network connection is restored, and the JobScheduler then takes care of ensuring the job runs when the network is restored.)

setOverrideDeadline() is not a solution?

I can see that JobInfo.Builder does have a setOverrideDeadline() method, which is almost what I want. But that specifies the maximum delay from when the job is scheduled (i.e. run the job in 10 seconds' time even if all constraints are not met), and not from when all constraints have been satisfied (i.e. run the job within 10 seconds of all constraints being satisfied).

EDIT: and there seems to be an annoying bug which can result in the job being run twice when using setOverrideDeadline(): see here and here.

What about Firebase JobDispatcher?

I see that Firebase JobDispatcher has a Trigger.NOW trigger ("means that the Job should be run as soon as its runtime constraints are satisfied"). Perhaps that's the way to go if JobSchedulerdoesn't support this natively? I've been put off by Firebase JobDispatcher because it seems like it's using a sledgehammer to crack a nut... and it appears that Firebase is all about cloud messaging etc, which is very far removed from local task scheduling (which should be an entirely local concern). And it seems to require Google Play services, which again seems completely unnecessary for local task scheduling. Furthermore, if immediate triggering it possible with Firebase, and Firebase just uses JobScheduler for Android L+, then it must surely be possible to do this directly with JobScheduler without relying on Firebase?

EDIT: I've now tried this, and even Trigger.NOW doesn't guarantee an immediate response... in fact I am finding that there is a delay of almost exactly 30 seconds on my device, which is odd.

Failing that...

At present, the only way I can see to ensure immediate execution (if constraints are met) is to not use JobScheduler.

Or maybe do the initial constraints check manually, and run the job with a setOverrideDeadline() of 0 if all constraints are met, otherwise run it without setOverrideDeadline().

It would seem far preferable just to have the ability to control the timing of JobScheduler itself, a bit like you can with the setWindow() method of AlarmManager.

Community
  • 1
  • 1
drmrbrewer
  • 11,491
  • 21
  • 85
  • 181
  • @OP could you please organize your post into smaller sub categories about what you are trying to say with respect to the problem you are trying to fix? – JoxTraex Sep 08 '16 at 08:00
  • @JoxTraex I had already attempted that (short version v long version) but I've added more substructure now. Nobody need read beyond the short version, but the long version is there for more background. – drmrbrewer Sep 08 '16 at 08:42
  • As you suggested, I suspect that this lack of API is by design. You're certainly welcome to ask for it as a feature enhancement, though any such enhancement would not show up until Android O at best (presumably), meaning it'll be 2022 before such a feature becomes commonplace. "maybe do the initial constraints check manually, and run the job with a setOverrideDeadline() of 0 if all constraints are met, otherwise run it without setOverrideDeadline()" -- or, just do the work if the constraints are met, and skip the job. No sense in using `JobScheduler` for immediate work. – CommonsWare Sep 10 '16 at 20:17
  • So @CommonsWare if I have a `JobService` set up to carry out the scheduled job, is there any way of running that directly, not via a `JobScheduler`, if I want it just to run *now*? I hate code duplication, and to create a separate `Service` for this (even with shared functions) seems inelegant.... much neater just to start the `JobService` directly? – drmrbrewer Sep 10 '16 at 21:08
  • "if I want it just to run now?" -- AFAIK, you can call `startService()` on your own `JobService`. Refactor the real work to be done into a separate class (for your background thread), one that can be created and used from `onStartJob()` or from `startService()`. It doesn't look like you can create a `JobParameters` yourself, so you cannot have `startService()` call `onStartJob()`, unless you passed `null` for the `JobParameters`. – CommonsWare Sep 10 '16 at 21:12
  • OK. I suppose that it's only necessary to call `jobFinished(params, false)` when started with `onStartJob(params)` so it doesn't matter that if you start it instead with `startService()` you don't have `params` available? You'd use `stopSelf()` as with a plain vanilla `Service`? – drmrbrewer Sep 10 '16 at 21:19
  • The other problem @CommonsWare is that multiple calls to `startService(myIntent)` result in only one `Service` being instantiated, so you have to manage the concurrent requests yourself (to work out when to call `stopSelf()`), whereas this problem doesn't exist when just scheduling multiple jobs via `JobScheduler`... each spawns its own instance of the `JobService`? Much easier if you could just add an "immediate" constraint to the `JobInfo` and use the same method both for immediate and non-immediate cases. Forgive me if I've got this (or the terminology) horribly wrong. – drmrbrewer Sep 10 '16 at 21:31
  • "each spawns its own instance of the JobService?" -- no more than `startService()` would. Limitations on `Service` are also limitations on all subclasses of `Service`, such as `JobService`. If you call `startService()` 10000 times in 2 seconds, you wind up with one `Service`. If 10000 jobs all fire in a 2 second window, you wind up with one `JobService`. If job concurrency is an issue, you need to handle that in either case (single-thread `ThreadPoolExecutor` to serialize them, appropriate concurrency controls to allow work to be done in parallel, etc.). – CommonsWare Sep 10 '16 at 21:38
  • @CommonsWare ah, OK thanks for the insight regarding concurrency. – drmrbrewer Sep 10 '16 at 21:42
  • But @CommonsWare do the "ordering" warnings regarding `stopSelf(startId)` (see https://developer.android.com/reference/android/app/Service.html#stopSelfResult(int)) apply also to `jobFinished(params)`? If I start jobA then jobB but jobA overruns, and I call `jobFinished(paramsForJobB)` while jobA is still running, will the `JobService` be killed while jobA is still running? – drmrbrewer Sep 10 '16 at 21:51
  • I don't know. I don't think that behavior is documented. – CommonsWare Sep 10 '16 at 21:54
  • To boldly go... – drmrbrewer Sep 10 '16 at 22:01
  • Would have been a lot more simple if `Trigger.NOW` of Firebase JobDispatcher actually meant what it says! On my device it is consistently triggering around 30 seconds after 'NOW', so it doesn't even seem to be resource dependent (it would vary more?)... just annoyingly "delayed a little" rather than "now". – drmrbrewer Sep 10 '16 at 22:06

1 Answers1

-1

A job scheduler is to schedule jobs: triggering periodically, with delay or with constraints to other jobs. If you want to fire a job instantly, it doesn't need to bee scheduled, just launch it.

ConnectivityManager cm =
            (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = cm.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnectedOrConnecting()) {

        // If there is connectivity, Launch the job directly here

    } else {

        PersistableBundle extras = new PersistableBundle();
        extras.putInt("anExtraInt", someInt);
        int networkConstraint = useUnmetered ?       
        JobInfo.NETWORK_TYPE_UNMETERED : JobInfo.NETWORK_TYPE_ANY;

        ComponentName componentName = new ComponentName(context,MyJobService.class);
        JobInfo jobInfo = new JobInfo.Builder(jobId, componentName)
                .setRequiredNetworkType(networkConstraint)
                .setExtras(extras)
                .build();

        JobScheduler jobScheduler = (JobScheduler)      context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        jobScheduler.schedule(jobInfo);
    }