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 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
.
Avoiding JobId Collisions
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.
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(...)
.
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
Bagikan Berita Ini
0 Response to "Working with Multiple JobServices"
Post a Comment