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.
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.
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.
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 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.
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.
onStartJob()
is called when the time (condition) for the scheduled task comes. This method is called in the main thread and the developer must independently carry out any heavy operations into separate threads (in our case, this is already provided for - we use the IntentService ). When delegating the task to other threads from onStartJob()
you must return true , and if all the necessary actions have already been performed in the body of this method, you need to return false .onStopJob()
is called when the required conditions for the task have ceased to be performed or the time allotted for the task has been exhausted. Calling this method informs the service that all background tasks should immediately stop running. It is best to provide secure stop execution logic to ensure data integrity.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.
To create a task for JobScheduler, you need JobInfo.Builder . Its constructor takes two parameters: the task ID and the ComponentName of our ExerciseJobService .
With an identifier, everything is simple (but not without nuances) - any integer value:
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);
Below we consider the main set of methods JobInfo.Builder.
exerciseJobBuilder.setMinimumLatency(TimeUnit.SECONDS.toMillis(1));
the minimum time that will pass before the task is completed, in other words, the delayed start time.
exerciseJobBuilder.setOverrideDeadline(TimeUnit.SECONDS.toMillis(5));
maximum time during which the task can be in the queue / scheduled state. If after 5 seconds (in our case) the favorable conditions have not come, the task will begin to be executed no matter what (if this does not contradict other JobScheduler policies). If not used, then the task will be performed only when the necessary conditions occur.
exerciseJobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
connection type is set, for example, we need internet, but for it to be free WIFi (not hotspot) or Ethernet, then we choose NETWORK_TYPE_UNMETERED
.
exerciseJobBuilder.setRequiresDeviceIdle(false);
Determines the state when the user does not interact with the device, in our case it does not matter.
exerciseJobBuilder.setRequiresCharging(false);
In our example, we assume that it makes no difference to us whether the device is charging or not.
You can set a retry criterion for the task (more on this below):
exerciseJobBuilder.setBackoffCriteria(TimeUnit.SECONDS.toMillis(10), JobInfo.BACKOFF_POLICY_LINEAR);
In addition, it is possible to make the task periodic:
exerciseJobBuilder.setPeriodic();
When we set the task periodicity, we are faced with logical constraints:
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.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.These are the main differences of the periodic task from the usual one. In the current example, we will not make the task periodic.
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 .
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.
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 .
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