10/6/17

Working with Multiple JobServices

Posted by Isai Damier, Software Engineer, Android DA

Working with Multiple JobServices



In its continuous effort to improve user experience, the Android platform has
introduced strict limitations on background services starting in API level 26.
Basically, unless your app is running in the href="https://developer.android.com/about/versions/oreo/background.html#services">foreground,
the system will stop all of your app's background services within minutes.



As a result of these restrictions on background services,
JobScheduler jobs have become the de facto solution for performing
background tasks. For people familiar with services, JobScheduler
is generally straightforward to use: except in a few cases, one of which we
shall explore presently.



Imagine you are building an Android TV app. Since channels are very important to
TV Apps, your app should be able to perform at least five different background
operations on channels: publish a channel, add programs to a channel, send logs
about a channel to your remote server, update a channel's metadata, and delete a
channel. Prior to Android 8.0 (Oreo) each of these five operations could be
implemented within background services. Starting in API 26, however, you must be
judicious in deciding which should be plain old background Services
and which should be JobServices.



In the case of a TV app, of the five operations mentioned above, only channel
publication can be a plain old background service. For some context, channel
publication involves three steps: first the user clicks on a button to start the
process; second the app starts a background operation to create and submit the
publication; and third, the user gets a UI to confirm subscription. So as you
can see, publishing channels requires user interactions and therefore a visible
Activity. Hence, ChannelPublisherService could be an IntentService
that handles the background portion. The reason you should not use a
JobService here is because JobService will introduce a
delay in execution, whereas user interaction usually requires immediate response
from your app.



For the other four operations, however, you should use JobServices;
that's because all of them may execute while your app is in the background. So
respectively, you should have ChannelProgramsJobService,
ChannelLoggerJobService, ChannelMetadataJobService,
and ChannelDeletionJobService.


Avoiding JobId Collisions



Since all the four JobServices above deal with Channel
objects, it should be convenient to use the channelId as the
jobId for each one of them. But because of the way
JobServices are designed in the Android Framework, you can't. The
following is the href="https://developer.android.com/reference/android/app/job/JobInfo.Builder.html#JobInfo.Builder(int,
android.content.ComponentName)">official description of jobId




class="prettyprint">Application-provided id for this job. Subsequent calls to cancel,
or jobs created with the same jobId, will update the pre-existing
job with the same id. This ID must be unique across all clients
of the same uid (not just the same package). You will want to
make sure this is a stable id across app updates, so probably not
based on a resource ID.


What the description is telling you is that even though you are using 4
different Java objects (i.e. -JobServices), you still cannot use the same
channelId as their jobIds. You don't get credit for
class-level namespace.



This indeed is a real problem. You need a stable and scalable way to relate a
channelId to its set of jobIds. The last thing you
want is to have different channels overwriting each other's operations because
of jobId collisions. Were jobId of type String instead
of Integer, the solution would be easy: jobId= "ChannelPrograms" +
channelId
for ChannelProgramsJobService, jobId= "ChannelLogs" +
channelId
for ChannelLoggerJobService, etc. But since
jobId is an Integer and not a String, you have to devise a clever
system for generating reusable jobIds for your jobs. And for that,
you can use something like the following JobIdManager.



JobIdManager is a class that you tweak according to your app's
needs. For this present TV app, the basic idea is to use a single
channelId over all jobs dealing with Channels. To
expedite clarification: let's first look at the code for this sample
JobIdManager class, and then we'll discuss.




class="prettyprint">public class JobIdManager {

public static final int JOB_TYPE_CHANNEL_PROGRAMS = 1;
public static final int JOB_TYPE_CHANNEL_METADATA = 2;
public static final int JOB_TYPE_CHANNEL_DELETION = 3;
public static final int JOB_TYPE_CHANNEL_LOGGER = 4;

public static final int JOB_TYPE_USER_PREFS = 11;
public static final int JOB_TYPE_USER_BEHAVIOR = 21;

@IntDef(value = {
JOB_TYPE_CHANNEL_PROGRAMS,
JOB_TYPE_CHANNEL_METADATA,
JOB_TYPE_CHANNEL_DELETION,
JOB_TYPE_CHANNEL_LOGGER,
JOB_TYPE_USER_PREFS,
JOB_TYPE_USER_BEHAVIOR
})
@Retention(RetentionPolicy.SOURCE)
public @interface JobType {
}

//16-1 for short. Adjust per your needs
private static final int JOB_TYPE_SHIFTS = 15;

public static int getJobId(@JobType int jobType, int objectId) {
if ( 0 < objectId && objectId < (1<< JOB_TYPE_SHIFTS) ) {
return (jobType << JOB_TYPE_SHIFTS) + objectId;
} else {
String err = String.format("objectId %s must be between %s and %s",
objectId,0,(1<<JOB_TYPE_SHIFTS));
throw new IllegalArgumentException(err);
}
}
}


As you can see, JobIdManager simply combines a prefix with a
channelId to get a jobId. This elegant simplicity,
however, is just the tip of the iceberg. Let's consider the assumptions and
caveats beneath.



First insight: you must be able to coerce channelId into a Short,
so that when you combine channelId with a prefix you still end up
with a valid Java Integer. Now of course, strictly speaking, it does not have to
be a Short. As long as your prefix and channelId combine into a
non-overflowing Integer, it will work. But margin is essential to sound
engineering. So unless you truly have no choice, go with a Short coercion. One
way you can do this in practice, for objects with large IDs on your remote
server, is to define a key in your local database or content provider and use
that key to generate your jobIds.



Second insight: your entire app ought to have only one JobIdManager
class. That class should generate jobIds for all your app's jobs:
whether those jobs have to do with Channels, Users, or
Cats and Dogs. The sample JobIdManager
class points this out: not all JOB_TYPEs have to do with
Channel operations. One job type has to do with user prefs and one
with user behavior. The JobIdManager accounts for them all by
assigning a different prefix to each job type.



Third insight: for each -JobService in your app, you must have a
unique and final JOB_TYPE_ prefix. Again, this must be an
exhaustive one-to-one relationship.


Using JobIdManager



The following code snippet from ChannelProgramsJobService
demonstrates how to use a JobIdManager in your project. Whenever
you need to schedule a new job, you generate the jobId using
JobIdManager.getJobId(...).



class="prettyprint">import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.PersistableBundle;

public class ChannelProgramsJobService extends JobService {

private static final String CHANNEL_ID = "channelId";
. . .

public static void schedulePeriodicJob(Context context,
final int channelId,
String channelName,
long intervalMillis,
long flexMillis)
{
JobInfo.Builder builder = scheduleJob(context, channelId);
builder.setPeriodic(intervalMillis, flexMillis);

JobScheduler scheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (JobScheduler.RESULT_SUCCESS != scheduler.schedule(builder.build())) {
//todo what? log to server as analytics maybe?
Log.d(TAG, "could not schedule program updates for channel " + channelName);
}
}

private static JobInfo.Builder scheduleJob(Context context,final int channelId){
ComponentName componentName =
new ComponentName(context, ChannelProgramsJobService.class);
final int jobId = JobIdManager
.getJobId(JobIdManager.JOB_TYPE_CHANNEL_PROGRAMS, channelId);
PersistableBundle bundle = new PersistableBundle();
bundle.putInt(CHANNEL_ID, channelId);
JobInfo.Builder builder = new JobInfo.Builder(jobId, componentName);
builder.setPersisted(true);
builder.setExtras(bundle);
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
return builder;
}

...
}


Footnote: Thanks to Christopher Tate and Trevor Johns for their invaluable
feedback

Load disqus comments

0 comments

loading...