Evolution of task planners



    The iFunny application we are working on has been available in more than five years. During this time, the mobile team had to go through a lot of different approaches and migrations between the tools, and a year ago it was time to switch from a samopisny decision and look in the direction of something more “fashionable” and common. This article is a small squeeze of what has been learned, what solutions they looked at and what they came to.

    Why do we need all this?

    Let's immediately determine what this article is for and why this topic turned out to be important for the Android development team:

    1. There are many scenarios when you need to run tasks outside the active user interface;
    2. the system imposes a large number of restrictions on the launch of such tasks;
    3. It was rather difficult to choose between existing solutions, since each tool has its own pros and cons.

    Chronology of events


    Android 0

    AlarmManager, Handler, Service


    Initially, their solutions for running background-based tasks on services were implemented. There was also a mechanism that tied tasks to the life cycle and knew how to cancel and restore them. It suited the team for a long time, as the platform did not impose any restrictions on such tasks.
    Google advised to do this based on the following diagram:



    At the end of 2018, there is no point in understanding this, it is enough to estimate the scale of the disaster.
    In fact, nobody cared how much work was going on in the background. Applications did what they wanted and when they wanted.

    Pros :
    available everywhere;
    available to all.

    Cons : the
    system in every way limits the work;
    no launches by condition;
    The API is minimal and you need to write a lot of code.

    Android 5. Lollipop

    JobScheduler


    After 5 (!) Years, closer to 2015, Google noticed that tasks are launched ineffectively. Users regularly complain that their phones are discharged simply by lying on a table or in a pocket.

    With the release of Android 5, a tool such as JobScheduler appeared. This is a mechanism with whose help it is possible to perform various tasks in the background, the start of which has been optimized and simplified due to the centralized system for launching these tasks and the ability to set conditions for this launch itself.

    In the code, it all looks quite simple: the service is announced, to which start and end events arrive.
    From the nuances: if you want to do the work asynchronously, then from onStartJob you need to start the stream; the main thing is not to forget to call the jobFinished method at the end of the work, otherwise the system will not let go of WakeLock, your task will not be considered completed and will be lost.

    publicclassJobSchedulerServiceextendsJobService{
        @OverridepublicbooleanonStartJob(JobParameters params){
            doWork(params);
            returnfalse;
        }
        @OverridepublicbooleanonStopJob(JobParameters params){
            returnfalse;
        }
    }

    From anywhere in the application you can initiate the execution of this work. Tasks are performed in our process, but are initiated at the IPC level. There is a centralized mechanism that controls their execution and wakes up the application only at the necessary points. You can also set different trigger conditions and transfer data through the Bundle.

    JobInfo task = new JobInfo.Builder(JOB_ID, serviceName)
            .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build();
    JobScheduler scheduler = 
    (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
    scheduler.schedule(task);

    In general, compared to nothing, it was already something. But this mechanism is only available with API 21, and at the time of release of Android 5.0, it would be strange to stop supporting all old devices (3 years have passed, and we still support fours).

    Pros :
    API is simple;
    conditions to run.

    Cons :
    available starting at API 21;
    in fact, only with API 23;
    easy to make a mistake.

    Android 5. Lollipop

    GCM Network Manager


    Also was presented an analogue of JobScheduler - GCM Network Manager. This is a library that provided similar functionality, but has already worked with API 9. However, in return, it required the availability of Google Play Services. Apparently, the functionality necessary for the JobScheduler operation began to be delivered not only through the Android version, but also at the GPS level. It should be noted that the framework developers quickly thought better of it and decided not to associate their future with GPS. Thank them for that.

    Everything looks absolutely identical. Same service:

    publicclassGcmNetworkManagerServiceextendsGcmTaskService{
        @OverridepublicintonRunTask(TaskParams taskParams){
            doWork(taskParams);
            return0;
        }
    }
    

    The same task launch:

    OneoffTask task = new OneoffTask.Builder()
            .setService(GcmNetworkManagerService.class)
            .setTag(TAG)
            .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
            .setRequiresCharging(true)
            .build();
    GcmNetworkManager mGcmNetworkManager = 
    GcmNetworkManager.getInstance(this);
    mGcmNetworkManager.schedule(task);

    Such a similarity of architecture was dictated by the inherited functionality and the desire to get a simple migration between tools.

    Pros :
    API, similar to JobScheduler;
    available starting from API 9.

    Cons :
    you must have Google Play Services;
    easy to make a mistake.


    Android 5. Lollipop

    WakefulBroadcastReceiver


    Next, I will write a few words about one of the basic mechanisms that is used in JobScheduler and is available to developers directly. This is WakeLock and WakefulBroadcastReceiver based on it.

    With the help of WakeLock, you can prevent the system from going to suspend, that is, keep the device active. This is necessary if we want to do some important work.
    When you create a WakeLock, you can specify its settings: keep the CPU, screen or keyboard.

    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE)
    PowerManager.WakeLock wl = pm.newWakeLock(PARTIAL_WAKE_LOCK, "name")
    wl.acquire(timeout);

    Based on this mechanism, the WakefulBroadcastReceiver works. We start the service and hold WakeLock.

    publicclassSimpleWakefulReceiverextendsWakefulBroadcastReceiver{
        @OverridepublicvoidonReceive(Context context, Intent intent){
            Intent service = new Intent(context, SimpleWakefulService.class);
            startWakefulService(context, service);
        }
    }

    After the service has completed the necessary work, we release it through similar methods.

    After 4 versions, this BroadcastReceiver will become deprecated, and the following alternatives will be described on developer.android.com:

    • JobScheduler;
    • SyncAdapter;
    • DownloadManager;
    • FLAG_KEEP_SCREEN_ON for Window.

    Android 6. Marshmallow

    DozeMode: sleep on the go


    Next, Google began to apply various optimizations for applications running on the device. But what is optimization for the user is a restriction for the developer.

    First of all, DozeMode appeared, which puts the device into sleep mode if it lay without action for a certain time. In the first versions, this lasted an hour, in subsequent periods, the duration of sleep was reduced to 30 minutes. Periodically, the phone wakes up, performs all pending tasks and falls asleep again. The DozeMode window grows exponentially. All transitions between modes can be tracked via adb.

    When DozeMode occurs, the following restrictions apply to the application:

    • the system ignores all WakeLock;
    • AlarmManager is delayed;
    • JobScheduler does not work;
    • SyncAdapter is not working;
    • access to the network is limited.

    You can also add your application to whitelist so that it does not fall under DozeMode restrictions, but at least Samsung completely ignored this list.

    Android 6. Marshmallow

    AppStandby: inactive applications


    The system identifies applications that are inactive and imposes all the same restrictions on them as in DozeMode.
    An application is sent to isolation if:

    • does not have a process in the foreground;
    • does not have active notification;
    • not added to the list of exceptions.

    Android 7. Nougat

    Background Optimizations. Svelte


    Svelte is a project in which Google is trying to optimize the consumption of RAM by applications and the system itself.
    In Android 7, within the framework of this project, it was decided that implicit broadcasts are not very effective, since a huge number of applications listen to them and the system spends a large amount of resources when these events occur. Therefore, the following event types were prohibited from being declared in the manifest:

    • CONNECTIVITY_ACTION;
    • ACTION_NEW_PICTURE;
    • ACTION_NEW_VIDEO.

    Android 7. Nougat

    FirebaseJobDispatcher


    At the same time, a new version of the framework for launching tasks was published - FirebaseJobDispatcher. In fact, it was added by GCM NetworkManager, which was put in order a bit and made a bit more flexible.

    Visually, everything looked exactly the same. Same service:

    publicclassJobSchedulerServiceextendsJobService{
        @OverridepublicbooleanonStartJob(JobParameters params){
            doWork(params);
            returnfalse;
        }
        @OverridepublicbooleanonStopJob(JobParameters params){
            returnfalse;
        }
    }

    The only thing that distinguished him was the possibility of installing his driver. The driver is the class that was responsible for the strategy for launching tasks.

    The very start of tasks over time has not changed.

    FirebaseJobDispatcher dispatcher = 
      new FirebaseJobDispatcher(new GooglePlayDriver(context));
    Job task = dispatcher.newJobBuilder()
            .setService(FirebaseJobDispatcherService.class)
            .setTag(TAG)
            .setConstraints(Constraint.ON_UNMETERED_NETWORK, 
                         Constraint.DEVICE_IDLE)
            .build();
    dispatcher.mustSchedule(task);
    

    Pros :
    API, similar to JobScheduler;
    available starting from API 9.

    Cons :
    you must have Google Play Services;
    easy to make a mistake.

    I was hoping to be able to install my driver in order to get rid of GPS. We even searched, but eventually found the following:





    Google knows about it, but these tasks remain open for several years.

    Android 7. Nougat

    Android Job by Evernote


    As a result, the community could not stand it, and a self-written solution appeared in the form of a library from Evernote. It was not the only one, but it was the solution from Evernote that was able to prove itself and “broke into people”.

    Architecturally, this library was more convenient than its predecessors.
    There was an entity responsible for creating tasks. In the case of JobScheduler, they were created through reflection.

    classSendLogsJobCreator : JobCreator{
        override fun create(tag: String): Job? {
            when (tag) {
                SendLogsJob.TAG -> return SendLogsJob()
            }
            returnnull
        }
    }

    There is a separate class, which is the task itself. In JobScheduler, this was all dumped into a switch inside onStartJob.

    classSendLogsJob : Job() {
    	override fun onRunJob(params: Params): Result {
    		return doWork(params)
    	}
    }

    Running tasks is identical, but besides the inherited events, Evernote has also added its own ones, such as starting daily tasks, unique tasks, running within a window.

    new JobRequest.Builder(JOB_ID)
    		.setRequiresDeviceIdle(true)
    		.setRequiresCharging(true)
    		.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
    		.build()
    		.scheduleAsync();

    Pros :
    convenient API;
    supported on all versions;
    no need google play services.

    Cons :
    third-party solution.

    The guys actively supported their library. Although there were quite a few critical issues, she worked on all versions and on all devices. As a result, last year, our Android team chose a solution from Evernote, since the libraries from Google cut off a large layer of devices that they cannot support.
    Inside she worked on solutions from Google, in extreme cases with the AlarmManager.

    Android 8. Oreo

    Background Execution Limits


    Let's return to our limitations. With the advent of the new Android came new optimizations. The guys from Google found another problem. This time the whole thing turned out to be in services and broadcasts (yes, nothing new).

    • startService if applications in background
    • implicit broadcast in manifest

    Firstly, it was forbidden to start services from the background. In the "framework of the law" there were only foreground services. Services now can be said deprecated.
    The second limitation is the same broadcasts. This time, registration of ALL implicit broadcasts in the manifesto has become prohibited. Implicit Broadcast is a Broadcast that is not just for our application. For example, there is Action ACTION_PACKAGE_REPLACED, and there is ACTION_MY_PACKAGE_REPLACED. So, the first is implicit.

    But any broadcast can still be registered via Context.registerBroadcast.

    Android 9. Pie

    Workmanager


    This optimization has so far stopped. Perhaps the devices began to work quickly and carefully in terms of energy consumption; users may have complained less about it.
    In Android 9, the developers of the framework have thoroughly approached the tool for running tasks. In an attempt to solve all the pressing problems, the library for running the background task of the WorkManager was presented on Google I / O.

    Google has recently been trying to shape its vision of the architecture of an Android application and gives developers the tools they need to do this. So there were architectural components from LiveData, ViewModel and Room. WorkManager looks like a reasonable addition to their approach and paradigm.

    If we talk about how the WorkManager is arranged inside, there is no technological breakthrough in it. In fact, it is a wrapper of existing solutions: JobScheduler, FirebaseJobDispatcher and AlarmManager.

    createBestAvailableBackgroundScheduler
    static Scheduler createBestAvailableBackgroundScheduler(Context, WorkManager){
        if (Build.VERSION.SDK_INT >= MIN_JOB_SCHEDULER_API_LEVEL) {
            returnnew SystemJobScheduler(context, workManager);
        } 
        try {
                 return tryCreateFirebaseJobScheduler(context);
              } catch (Exception e) {
                 returnnew SystemAlarmScheduler(context);
        }
    }


    The selection code is pretty simple. But it should be noted that JobScheduler is available starting from API 21, but using it only with API 23, since the first versions were rather unstable.

    If the version is lower than 23, then through reflection we try to find FirebaseJobDispatcher, otherwise we use AlarmManager.

    It is worth noting, the wrapper came out quite flexible. This time, the developers broke everything into separate entities, and architecturally it looks comfortable:

    • Worker - work logic;
    • WorkRequest - task launch logic;
    • WorkRequest.Builder - parameters;
    • Constrains - conditions;
    • WorkManager - a manager who manages tasks;
    • WorkStatus - task status.




    Startup conditions inherited from JobScheduler.
    It can be noted that the trigger for changing the URI appeared only with API 23. In addition, you can subscribe to change not only a specific URI, but all nested in it using the flag in the method.

    If we talk about us, then at the alpha stage, it was decided to switch to the WorkManager.
    There are several reasons for this. Evernote has a couple of critical bugs that the library developers promise to fix with the transition to the version with the integrated WorkManager. Yes, and they agree that the decision from Google negates the advantages of Evernote. In addition, this solution fits well with our architecture, since we use Architecture Components.

    Further I would like to show, in a simple example, in what form we try to use this approach. It is not very critical, you WorkManager or JobScheduler.



    Let's look at an example with a very simple case: click on republish or like.

    Now all applications are trying to get away from blocking requests to the network, as this unnerves the user and makes him wait, although at this time he can make purchases inside the application or watch ads.

    In such cases, local data is first changed - the user immediately sees the result of his action. Then in the background there is a request to the server, if it fails, the data is reset to the initial state.

    Next, I will show an example of how it looks like with us.

    JobRunner contains logic for running tasks. Its methods describe the configuration of tasks and pass parameters.

    JobRunner.java
    fun likePost(content: IFunnyContent){
        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        val input = Data.Builder()
                .putString(LikeContentJob.ID, content.id)
                .build()
        val request = OneTimeWorkRequest.Builder(LikeContentJob::class.java)
                .setInputData(input)
                .setConstraints(constraints)
                .build()
        WorkManager.getInstance().enqueue(request)
    }
    


    The task itself within the WorkManager is as follows: we take the id from the parameters and call the method on the server to like this content.

    We have a base class that contains the following logic:

    abstractclassBaseJob : Worker() {
    	final override fun doWork(): Result {
    		val workerInjector = WorkerInjectorProvider.injector()
    		workerInjector.inject(this)
    		return performJob(inputData)
    	}
    	abstract fun performJob(params: Data): Result
    }
    

    Firstly, it allows you to get away a little from the obvious knowledge about the Worker. It also contains the logic of dependency injection through WorkerInjector.

    WorkerInjectorImpl.java
    @SingletonpublicclassWorkerInjectorImplimplementsWorkerInjector{
    	@InjectpublicWorkerInjectorImpl(){}
        @Ovierridepublicvoidinject(Worker job){
    		if (worker instanceof AppCrashedEventSendJob) {
    			Injector.getAppComponent().inject((AppCrashedEventSendJob) job);
    		} elseif (worker instanceof CheckNativeCrashesJob) {
    			Injector.getAppComponent().inject((CheckNativeCrashesJob) job);
    		}
    	}
    }
    


    He simply proxies calls to Dagger, but it helps us in testing: we replace the injector implementations and inject the necessary environment into the tasks.

    fun voidtestRegisterPushProvider(){
        WorkManagerTestInitHelper.initializeTestWorkManager(context)
        val testDriver = WorkManagerTestInitHelper.getTestDriver()
        WorkerInjectorProvider.setInjector(TestInjector()) // mock dependencies
        val id = jobRunner.runPushRegisterJob()
        testDriver.setAllConstraintsMet(id)
        Assert.assertTrue(…)
    }
    

    classLikePostInteractor @Injectconstructor(
            valiFunnyContentDao: IFunnyContentDao,
            valjobRunner: JobRunner) : Interactor{
        fun execute(){
            iFunnyContentDao.like(getContent().id)
            jobRunner.likePost(getContent())
        }
    }

    Interactor is an entity that ViewController pulls to initiate the passage of a script (in this case — like). We mark the content locally as “zaikanny” and send the task for execution. If the task fails, the like is removed.

    classIFunnyContentViewModel(valiFunnyContentDao: IFunnyContentDao) : ViewModel() {
        val likeState = MediatorLiveData<Boolean>()
        var iFunnyContentId = MutableLiveData<String>()
        privatevar iFunnyContentState: LiveData<IFunnyContent> = attachLiveDataToContentId();
        init {
            likeState.addSource(iFunnyContentState) { likeState.postValue(it!!.hasLike) }
        }
    }

    We use Google’s Architecture Components: ViewModel and LiveData. This is our ViewModel. Here we associate the update of the object in the DAO with the status of the like.

    IFunnyContentViewController.java
    classIFunnyContentViewController @Injectconstructor(
            privatevallikePostInteractor: LikePostInteractor,
            valviewModel: IFunnyContentViewModel) : ViewController{
        override fun attach(view: View){
            viewModel.likeState.observe(lifecycleOwner, { updateLikeView(it!!) })
        }
        fun onLikePost(){
            likePostInteractor.setContent(getContent())
            likePostInteractor.execute()
        }
    }


    ViewController, on the one hand, subscribes to a change in the status of a like, on the other hand, it initiates the passage of the script we need.

    And this is almost all the code we need. It remains to add the behavior of the View itself with a like and implementation of your DAO; if you use Room, then simply register the fields in the object. It looks quite simple and effective.

    If to sum up


    JobScheduler, GCM Network Manager, FirebaseJobDispatcher:

    • don't use them
    • don't read any more articles about them
    • do not watch reports
    • don't think which one to choose.

    Android Job by Evernote:

    • inside will use WorkManager;
    • critical bugs are blurred between solutions.

    WorkManager:

    • API LEVEL 9+;
    • does not depend on Google Play Services;
    • Chaining / InputMergers;
    • reactive approach;
    • support from Google (I want to believe it).

    Also popular now: