05 October 2017
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 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 Service
s
and which should be JobService
s.
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 JobService
s;
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
.
Since all the four JobService
s 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
JobService
s are designed in the Android Framework, you can't. The
following is the official description of jobId
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 jobId
s. 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 jobId
s. 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 jobId
s 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 Channel
s. To
expedite clarification: let's first look at the code for this sample
JobIdManager
class, and then we'll discuss.
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 jobId
s.
Second insight: your entire app ought to have only one JobIdManager
class. That class should generate jobId
s for all your app's jobs:
whether those jobs have to do with Channel
s, User
s, or
Cat
s and Dog
s. The sample JobIdManager
class points this out: not all JOB_TYPE
s 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.
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(...)
.
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