We always need a smooth user experience, but it’s a shame that the hardware resources we have at our disposal are always in conflict with that need. This is where the Android platform’s ongoing efforts come in — starting with API 26, Android introduced strict restrictions on backend services. Basically, unless your application is running in the foreground, the system will stop all background services for your application within a few minutes.
Because of these limitations on background services, JobScheduler has become a practical solution for performing background tasks. JobScheduler is usually simple to use for developers familiar with the service, with a few exceptions. We’ll look at one of those exceptions this time.
Suppose you’re building an Android TV application. Channels are important to TV applications, so your application needs to be able to perform at least five channel-related background operations: publish a channel, add a show to a channel, send a log of the channel to a remote server, update the metadata for the channel, and delete the channel. Prior to Android 8.0 (Oreo), each of these five operations could be implemented in background services. However, starting with API 26, you have to be smart about which ones should stick with plain old background services and which ones should use JobService.
If only the use of TV App is considered, of the above five operations, only “channel publishing” can be made into an original ordinary background service. In some cases, channel publishing involves three steps: the user clicks a button to begin the process; The application then launches a background operation to create and submit the publication; Finally, the user confirms the subscription through the user interface. As you can see so far, publishing channels requires user interaction, so you need visible activities. So the ChannelPublisherService could be an IntentService that handles the background logic. You should not use JobService here because JobService introduces latency, and user interactions typically require an immediate response from your application.
For the other four operations, you should use JobService; Because they can all be executed while your application is in the background. So you should create ChannelProgramsJobService respectively, ChannelLoggerJobService, ChannelMetadataJobService, and ChannelDeletionJobService.
Avoid JobId conflicts
Since all four jobServices above deal with Channel objects, it seems convenient for you to use the channelId as a jobId. But because of the way JobService is designed in the Android Framework, you can’t do that. Here’s jobId’s official description:
Apply the ID provided for this job. Subsequent calls to cancel, or to create a job with the same jobId, will update existing jobs with the same ID. This ID must be unique across all clients with the same UID (not just the same application package). You need to ensure that this ID is always stable when applying updates, so it probably shouldn't be based on the resource ID.Copy the code
Based on the above description, even if you use four different Java objects (that is, -jobService), you still cannot use channelId as their jobId. Class-level namespaces do not help you.
That’s a real problem. You need a stable, extensible way to associate channelId with its jobId. At worst, different channels overwrite each other due to jobId conflicts. If jobId is a String instead of an Integer, this is easy to solve: ChannelProgramsJobService jobId = “ChannelPrograms” + channelId, ChannelLoggerJobService jobId = “ChannelLogs” + channelId, etc. But because jobId is of type Integer, not String, you need to design an intelligent system to generate reusable Jobids for your jobs.
Here’s the point — now let’s talk about JobIdManager and see how it can be used to solve this problem.
JobIdManager is a category that you can tailor to your own application needs. The basic idea for the TV application mentioned so far is to use a single channelId to handle all jobs associated with the Channel. Let’s take a look at the code for the sample JobIdManager class and then discuss it in more detail.
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); }}}Copy the code
As you can see, the jobId Manager simply combines a prefix and a channelId to get the jobId. Yet this simple and elegant solution is just the tip of the iceberg. Let’s think about the assumptions and considerations.
You must be able to force the channelId to be of type Short, so when you combine the channelId with a prefix, you still get a valid Java Integer. Of course, strictly speaking, it doesn’t have to be Short. It works as long as your prefix and channelId are combined into a non-overflow Integer. But marginal processing is critical in solid software engineering. So unless you are desperate, force Short. In practice, one way to do this for objects with large ids on remote servers is to define a key in a local database or content provider and use that key to generate your jobId.
Your entire application should have only one JobIdManager class. This class generates jobids for all jobs in the application: whether they are related to channels, users, or anything else. In fact, our example JobIdManager class points this out: not all JOB_TYPE are associated with Channel operations. One job type is related to user preferences and one to user behavior. JobIdManager overrides these types by assigning a different prefix to each job type.
Each -JobService in your application must have a unique and final JOB_TYPE_ prefix. Again, it has to be a complete one-to-one relationship.
Using JobIdManager
The following code snippets ChannelProgramsJobService, it for our demonstrates how to use JobIdManager in your project. Jobidmanager.getjobid (…) is used whenever a new job needs to be scheduled. Generate the jobId.
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);
returnbuilder; }... }Copy the code
This should give you a clearer idea of how to design the backend mechanism for different scenarios. Either way, the restrictions on background tasks since Oreo have practical implications for improving the user experience, requiring developers to be more precise about what their apps need to do and when they need to do it. If you have any questions or experiences, please share them with us below
* Note: Thanks to Christopher Tate and Trevor Johnsz for their valuable feedback during the writing of this article