📜 ⬆️ ⬇️

Scheduling tasks in Android using JobScheduler and IntentService


Sometimes, when developing for the Android OS, it is necessary to perform resource-intensive operations periodically, regularly or on demand, and for these operations it is important, for example, that the Internet is available or that the device “does not sleep”. Most often, when solving such problems, AlarmManager, WakefulBroadcastReceiver are used, or else WakeLock is controlled manually. All this is not recommended in the documentation for developers for Android, and WakefulBroadcastReceiver is already marked as deprecated with API level 26.0.0.


So, what can we do to follow Google’s recommendations and create more flexible applications on Android 5.0+, in which more attention is paid to energy conservation? If you are ready to set the minimum API level 21.0.0 for your application, I suggest using an example of using JobScheduler in conjunction with IntentService for consistently performing laborious tasks under the cat.


The crux of the issue and raised issues


It is enough to open the documentation on the WakefulBroadcastReceiver class to see an interesting situation: the class is added in version 22.0.0 of the API, and marked as deprecated in version 26.0.0. We can assume that, at first, Android developers decided to add a convenient class for performing tasks with WakeLock retention, but then it turned out that no one guarantees that the application will work in the foreground, and indeed when every "smart" application tries to keep WakeLock , when device save energy? And the principle of work of WakefulBroadcastReceiver began to go against the urge to extend the battery charge life, because, among other things, an unclosed receiver could lead to WakeLock 's leaks.


On the other hand, ordinary developers may wonder how it is safe to perform periodic tasks with all the limitations, for example, when Doze mode is active? In order to balance the balance of limitations and opportunities, JobScheduler was created, which takes upon itself the question of when the task can be completed, how appropriately it can be done, if it is really needed, and how not to violate the energy saving policy and not lose where Anyone not let go of WakeLock .


While this article was being prepared, an article by another author appeared on Habré in which a little more theory was revealed and a little less attention was paid to practice. It will be useful for a quick start and a deeper understanding of JobScheduler ’s existing alternatives.


An example of creating a project with JobScheduler and IntentService


For the sake of simplicity, let us imagine that we have the task of recording the word "Exercise" to a file, while we need the Internet, and preferably not mobile, because our task is not worth wasting mobile user traffic. At the same time, we want each new task to record a word to be performed one after another. Since we can’t block the main thread (here, work with files and, possibly, some network requests), and our tasks can line up, IntentService comes to help .


To fully reproduce the steps described in this article, you need to create a new project without activation, with a minimum version of SDK 21. Also, all actions can be performed in an existing project with a minimum version of SDK no lower than 21.


Adding an IntentService to handle tasks in the background


Add a new IntentService to the project, call it ExerciseIntentService , use the standard approach based on automatically generated methods, clean up the excess and rename the methods and constants under our conditions.


As a result of simple manipulations, we obtain the following body for ExerciseIntentService :


private static final String ACTION_WRITE_EXERCISE = "com.github.ihandy.jobschedulerdemo.action.ACTION_WRITE_EXERCISE"; public ExerciseIntentService() { super("ExerciseIntentService"); } public static void startActionWriteExercise(Context context) { Intent intent = new Intent(context, ExerciseIntentService.class); intent.setAction(ACTION_WRITE_EXERCISE); context.startService(intent); } @Override protected void onHandleIntent(Intent intent) { if (intent != null) { final String action = intent.getAction(); if (ACTION_WRITE_EXERCISE.equals(action)) { handleActionWriteExercise(); } } } private void handleActionWriteExercise() { try { FileWriter fileWriter = new FileWriter(getFilesDir().getPath() + "exercise.txt"); fileWriter.write("Exercise"); fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } 

And so, the service that will directly perform our similarly conditionally-laborious tasks in order is ready. In order to create and schedule tasks, you need an entry point, which you can use as a BroadcastReceiver .


Add BroadcastReceiver to initialize tasks


Add the standard means BroadcastReciever and call it ExerciseRequestsReceiver . In the future, we will be able to send Broadcasts to our application from anywhere, so that it schedules tasks (for example, this can be done with the help of instrumental tests, which will be shown closer to the end of the article).


The minimum required ExerciseRequestsReceiver code looks like this:


 public static final String ACTION_PERFORM_EXERCISE = "ACTION_PERFORM_EXERCISE"; private static int sJobId = 1; @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case ACTION_PERFORM_EXERCISE: scheduleJob(context); break; default: throw new IllegalArgumentException("Unknown action."); } } 

String ACTION_PERFORM_EXERCISE - action to identify the need to start the task scheduling process.
int sJobId is the variable for the task identifier that will be used when scheduling tasks.
scheduleJob(context) - a method call that will contain all the necessary logic for scheduling a task.


Now, when we received Broadcast, we could send an intent to our ExerciseIntentService , and everything would be fine, only in our case WakeLock is needed, and since we agreed to no longer use WakefulBroadcastReceiver , we need to create and schedule a new task for JobScheduler , and then he will do everything for us (almost).


By the way, when using JobScheduler, you do not need permission to WakeLock , unlike other ways to solve this problem.


Implementing a JobService inheritor to handle events from JobScheduler


JobScheduler requires a separate service inherited from the JobService . Let's call it ExerciseJobService and add it as a regular service, replacing the parent class and adding permission to the module manifest:


 <service android:name=".ExerciseJobService" android:enabled="true" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE"> </service> 

Allowing android.permission.BIND_JOB_SERVICE requires that this service be able to interact with JobSchedule r.


In addition, two onStartJob() and onStopJob() methods are required for implementation.



To correctly handle the situation with onStopJob() you can implement static flags, any additional broadcasts, and you can also use other means of service interaction. In the current article this will be omitted and the assumption introduced that if our IntentService failed to complete the task, then nothing terrible for the application logic and data integrity will not occur.


onStopJob also has a return value, if this is true - then JobScheduler will place the interrupted task in the execution queue again, false - the task will be considered completed and will be removed from the queue if it was not periodic.


Since the processing of emergency situations is omitted, we will return true from this method so that the task is re-planned, at the same time it will allow us to consider the use of the repeat criterion.


Thus, by setting the return values ​​from the two main methods and adding the launch of our ExerciseIntentService in onStartJob() , we get the following sufficiently large service:


 public class ExerciseJobService extends JobService { public ExerciseJobService() { } @Override public boolean onStartJob(JobParameters params) { ExerciseIntentService.startActionWriteExercise(getApplicationContext()); return true; } @Override public boolean onStopJob(JobParameters params) { return true; } } 

At the moment, a minimal set of classes has been prepared for implementing the task through JobScheduler in the IntentService , namely: ExerciseIntentService - performs the operations directly required for the task in separate streams, ExerciseJobService - catches events from JobScheduler 'a and runs ExerciseIntentService , and ExerciseRequestsReceiver - catches events the work of our complex, where we catch broadcasts from the outside and have to initialize the task for JobScheduler , than we go ahead.


Create a new task for JobScheduler


To create a task for JobScheduler, you need JobInfo.Builder . Its constructor takes two parameters: the task ID and the ComponentName of our ExerciseJobService .


Task ID and ComponentName


With an identifier, everything is simple (but not without nuances) - any integer value:



Note about using UID

If suddenly your application is systemic, or you have several applications with one sharedUserId , then you need to take into account an additional condition: id should not intersect among all applications with one uid. Thus, if the application uses android.uid.system , then you need to take into account that some system tasks also use JobScheduler , and the uniqueness of id must be maintained independently.


By the way, when using such methods in JobScheduler as removeAll() we can delete other people's tasks with the same uid.


An article in English about how to control this situation.


In this example, you do not need to bother about the UID and the incremental value sJobId is used as an identifier.


sJobId is defined as follows:


 private static int sJobId = 1; 

With ComponentName, everything is much simpler, this is the object in the constructor of which ExerciseJobService.class is passed.


 ComponentName jobService = new ComponentName(context, ExerciseJobService.class); JobInfo.Builder exerciseJobBuilder = new JobInfo.Builder(sJobId++, jobService); 

Parameter initialization using JobInfo.Builder


Below we consider the main set of methods JobInfo.Builder.



You can set a retry criterion for the task (more on this below):



In addition, it is possible to make the task periodic:



Periodic Tasks


When we set the task periodicity, we are faced with logical constraints:


  1. setMinimumLatency() and setOverrideDeadline() cannot be used, since it does not make sense - the task should somehow be executed once during a given interval, and no additional restrictions from above or below are allowed. On the other hand, sometimes we need to wait for something, and then start a periodic task - here we cannot add such a condition, if you need to wait, it means you need to wait before adding a task for execution.
  2. in JobService in onStopJob() we can not return true - the interrupted periodic task will not be removed from the queue; next time it will be executed according to the schedule.
  3. No one guarantees that the task will be performed smoothly through the specified interval, it will simply be executed no more than 1 time during this interval.

These are the main differences of the periodic task from the usual one. In the current example, we will not make the task periodic.


Repeat task criterion


setBackoffCriteria() allows you to specify a rule that will be used to onStopJob() task execution if necessary (for example, in onStopJob() we returned true ).


JobScheduler offers us two policies: linear and exponential.


The formula for linear politics is as follows:


 retry_time(current_time, num_failures) = current_time + initial_backoff_millis * num_failures, num_failures >= 1 

those. from the current time, the next attempt will be made after a specified amount of time multiplied by the number of failures.


Formula exponential policy:


 retry_time(current_time, num_failures) = current_time + initial_backoff_millis * 2 ^ (num_failures - 1), num_failures >= 1 

Here the time of the next attempt grows in much larger steps.


Everything is quite simple and transparent, but what will happen if our task cannot be executed successfully many times, 10, 20 ...? With a given initial retry time of 1 minute to 10 attempts, it will take almost an hour. It is not easy enough to catch such situations, because we cannot predict what will happen in an hour. JobScheduler limits such repetitions to five hours.


Thus, the setBackoffCriteria() parameter should be handled very carefully, carefully considering the initial time and type of policy in accordance with the tasks. You may also have to perform additional processing, for example, the number of retries and delete the task from JobScheduler .


Submitting task for execution


Thus, we have JobBuilder ready with all the parameters we need. To add a task to the execution queue, you need to get a JobScheduler instance from the system:


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

And call the method to add JobInfo from the builder:


 jobScheduler.schedule(exerciseJobBuilder.build()); 

For reliability, you can check the return value of the last method, which tells us about the successful or not successful addition of the task to the queue.


Further, when JobScheduler finds that the conditions for performing the task are optimal, the onStartJob() method is onStartJob() in ExerciseJobService , which has already been parsed above.


Check and debug


To sum up, as a result, we got a test application that allows you to plan one-time tasks that perform a conditionally heavy operation in the IntentService .


To test the performance, I propose to add a small tool test that looks like this:


  @Test public void sendDemoBroadcast() throws Exception { Context appContext = InstrumentationRegistry.getTargetContext(); Intent demoIntentForBroadcast = new Intent(appContext, ExerciseRequestsReceiver.class); demoIntentForBroadcast .setAction(ExerciseRequestsReceiver.ACTION_PERFORM_EXERCISE); appContext.sendBroadcast(demoIntentForBroadcast); } 

Also, to check the work of the example, some logs were added to the source code, which are not listed above in the excerpts.


If we run the test, we will see that our task with "id: 1" starts and ends, starts and ends ... More precisely, it is forcibly completed by JobScheduler .


Executing a task in separate threads and notifying the service of completion


In this example, from the onStartJob() method, we returned true , which means that we told JobScheduler that the task continues somewhere in the sidestream. Since we are not notified of the completion of the task, JobScheduler terminates it forcibly, and since we also return true onStopJob() , the retry policy is triggered, and the task is rescheduled and restarted.


To prevent this from happening, you need to call the jobFinished() method in the ExerciseJobService service class, I will try to explain its use and the various options for transferring information about the completion of the task from the IntentService in the following articles.


This completes the creation of a test case, it is ready for use and application in work projects for scheduling tasks. IntentService was used here to perform tasks in the background, but other methods are possible, for example, using ThreadPoolExecutor or HandlerThread . And in the case of development exclusively for Android O and above, I recommend also to pay attention to the JobIntentService .


The full code for the example in question is provided on GitHub .


You can also read the official JobScheduler implementation example from the Activity on developer.android.com .




Illustration: Anni ART (copying and reproduction only with the consent of the author).


')

Source: https://habr.com/ru/post/339012/


All Articles