Simple Job Scheduler Demo in Android

By | January 20, 2018

Android helps us to schedule jobs in an efficient way. Android has a built in Job scheduler for this.
Lets see how this is done.

Before that lets see what google has to say about Job Scheduling.

These are the normal tasks an application does :

  • Updating network resources.
  • Downloading information.
  • Updating background tasks.
  • Scheduling system service calls.

Scheduling this work intelligently can improve your app’s performance, along with aspects of system health such as battery life. JobScheduler does this scheduling work for you.

Below are the other ways for doing periodic tasks

  • AlarmManager
  • Firebase JobDispatcher
  • SyncAdapter

You can read more about job scheduling from here…
https://developer.android.com/topic/performance/scheduling.html

Code

Main Activity that starts the Job.

package com.coderzheaven.android.jobscheduler;

import android.app.Activity;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.Toast;

import com.example.android.jobscheduler.service.MyJobService;

import java.lang.ref.WeakReference;
import java.util.List;

public class MainActivity extends Activity {

    private static final String TAG = MainActivity.class.getSimpleName();

    public static final int JOB_STARTED = 1;
    public static final int JOB_STOPPED = 2;

    public static final String MESSENGER_INTENT_KEY
            = BuildConfig.APPLICATION_ID + ".MESSENGER_INTENT_KEY";
    public static final String WORK_DURATION_KEY =
            BuildConfig.APPLICATION_ID + ".WORK_DURATION_KEY";

    private EditText mDelayEditText;
    private EditText mDeadlineEditText;
    private EditText mDurationTimeEditText;
    private RadioButton mWiFiConnectivityRadioButton;
    private RadioButton mAnyConnectivityRadioButton;
    private CheckBox mRequiresChargingCheckBox;
    private CheckBox mRequiresIdleCheckbox;

    private ComponentName mServiceComponent;

    private int mJobId = 0;

    // Handler for incoming messages from the service.
    private IncomingMessageHandler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sample_main);

        // Set up UI.
        mDelayEditText = (EditText) findViewById(R.id.delay_time);
        mDurationTimeEditText = (EditText) findViewById(R.id.duration_time);
        mDeadlineEditText = (EditText) findViewById(R.id.deadline_time);
        mWiFiConnectivityRadioButton = (RadioButton) findViewById(R.id.checkbox_unmetered);
        mAnyConnectivityRadioButton = (RadioButton) findViewById(R.id.checkbox_any);
        mRequiresChargingCheckBox = (CheckBox) findViewById(R.id.checkbox_charging);
        mRequiresIdleCheckbox = (CheckBox) findViewById(R.id.checkbox_idle);
        mServiceComponent = new ComponentName(this, MyJobService.class);

        mHandler = new IncomingMessageHandler(this);
    }

    @Override
    protected void onStop() {
        // A service can be "started" and/or "bound". In this case, it's "started" by this Activity
        // and "bound" to the JobScheduler (also called "Scheduled" by the JobScheduler). This call
        // to stopService() won't prevent scheduled jobs to be processed. However, failing
        // to call stopService() would keep it alive indefinitely.
        stopService(new Intent(this, MyJobService.class));
        super.onStop();
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Start service and provide it a way to communicate with this class.
        Intent startServiceIntent = new Intent(this, MyJobService.class);
        Messenger messengerIncoming = new Messenger(mHandler);
        startServiceIntent.putExtra(MESSENGER_INTENT_KEY, messengerIncoming);
        startService(startServiceIntent);
    }

    /**
     * Executed when user clicks on SCHEDULE JOB.
     */
    public void scheduleJob(View v) {
        JobInfo.Builder builder = new JobInfo.Builder(mJobId++, mServiceComponent);

        String delay = mDelayEditText.getText().toString();
        if (!TextUtils.isEmpty(delay)) {
            builder.setMinimumLatency(Long.valueOf(delay) * 1000);
        }
        String deadline = mDeadlineEditText.getText().toString();
        if (!TextUtils.isEmpty(deadline)) {
            builder.setOverrideDeadline(Long.valueOf(deadline) * 1000);
        }
        boolean requiresUnmetered = mWiFiConnectivityRadioButton.isChecked();
        boolean requiresAnyConnectivity = mAnyConnectivityRadioButton.isChecked();
        if (requiresUnmetered) {
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
        } else if (requiresAnyConnectivity) {
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
        }
        builder.setRequiresDeviceIdle(mRequiresIdleCheckbox.isChecked());
        builder.setRequiresCharging(mRequiresChargingCheckBox.isChecked());

        // Extras, work duration.
        PersistableBundle extras = new PersistableBundle();
        String workDuration = mDurationTimeEditText.getText().toString();
        if (TextUtils.isEmpty(workDuration)) {
            workDuration = "1";
        }
        extras.putLong(WORK_DURATION_KEY, Long.valueOf(workDuration) * 1000);

        builder.setExtras(extras);

        // Schedule job
        Log.d(TAG, "Scheduling job");
        JobScheduler tm = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        tm.schedule(builder.build());
    }

    /**
     * Executed when user clicks on CANCEL ALL.
     */
    public void cancelAllJobs(View v) {
        JobScheduler tm = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        tm.cancelAll();
        Toast.makeText(MainActivity.this, R.string.all_jobs_cancelled, Toast.LENGTH_SHORT).show();
    }

    /**
     * Executed when user clicks on FINISH LAST TASK.
     */
    public void finishJob(View v) {
        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        List<JobInfo> allPendingJobs = jobScheduler.getAllPendingJobs();
        if (allPendingJobs.size() > 0) {
            // Finish the last one
            int jobId = allPendingJobs.get(0).getId();
            jobScheduler.cancel(jobId);
            Toast.makeText(
                    MainActivity.this, String.format(getString(R.string.cancelled_job), jobId),
                    Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(
                    MainActivity.this, getString(R.string.no_jobs_to_cancel),
                    Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * A {@link Handler} allows you to send messages associated with a thread. A {@link Messenger}
     * uses this handler to communicate from {@link MyJobService}. It's also used to make
     * the start and stop views blink for a short period of time.
     */
    private static class IncomingMessageHandler extends Handler {

        // Prevent possible leaks with a weak reference.
        private WeakReference<MainActivity> mActivity;

        IncomingMessageHandler(MainActivity activity) {
            super(/* default looper */);
            this.mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity mainActivity = mActivity.get();
            if (mainActivity == null) {
                // Activity is no longer available, exit.
                return;
            }
            Message m;
            switch (msg.what) {
                /*
                 * Receives callback from the service when a job has landed
                 * on the app. Turns on indicator and sends a message to turn it off after
                 * a second.
                 */
                case JOB_STARTED:
                    Toast.makeText(mainActivity, "Job Started", Toast.LENGTH_SHORT).show();
                    break;
                case JOB_STOPPED:
                    Toast.makeText(mainActivity, "Job Stopped", Toast.LENGTH_SHORT).show();
                    break;

            }
        }

    }
}

Layout

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">


        <TableLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="8dp">

            <TableRow
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginRight="12dp"
                    android:gravity="right|center_vertical"
                    android:text="@string/work_duration"
                    android:textSize="14sp" />

                <EditText
                    android:id="@+id/duration_time"
                    android:layout_width="48dp"
                    android:layout_height="wrap_content"
                    android:inputType="number"
                    android:text="5" />

            </TableRow>

            <TableRow
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginRight="12dp"
                    android:gravity="right|center_vertical"
                    android:text="@string/connectivity"
                    android:textSize="14sp" />

                <RadioGroup
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal">

                    <RadioButton
                        android:id="@+id/checkbox_any"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:checked="true"
                        android:text="@string/any" />

                    <RadioButton
                        android:id="@+id/checkbox_unmetered"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/unmetered" />
                </RadioGroup>

            </TableRow>

            <TableRow
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginRight="12dp"
                    android:gravity="right|center_vertical"
                    android:text="@string/delay"
                    android:textSize="14sp" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:orientation="horizontal">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/timing" />

                    <EditText
                        android:id="@+id/delay_time"
                        android:layout_width="48dp"
                        android:layout_height="wrap_content"
                        android:inputType="number"
                        android:text="0" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/deadline"
                        android:textSize="14sp" />

                    <EditText
                        android:id="@+id/deadline_time"
                        android:layout_width="48dp"
                        android:layout_height="wrap_content"
                        android:inputType="number"
                        android:text="15" />

                </LinearLayout>

            </TableRow>

            <TableRow
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginRight="12dp"
                    android:gravity="right|center_vertical"
                    android:text="@string/charging_caption"
                    android:textSize="14sp" />

                <CheckBox
                    android:id="@+id/checkbox_charging"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/charging_text" />

            </TableRow>

            <TableRow
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginRight="12dp"
                    android:gravity="right|center_vertical"
                    android:text="@string/idle_caption" />

                <CheckBox
                    android:id="@+id/checkbox_idle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/idle_mode_text" />
            </TableRow>

        </TableLayout>

        <Button
            android:id="@+id/schedule_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="40dp"
            android:layout_marginTop="20dp"
            android:onClick="scheduleJob"
            android:text="@string/schedule_job_button_text" />

        <Button
            android:id="@+id/finished_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="40dp"
            android:onClick="finishJob"
            android:padding="20dp"
            android:text="@string/finish_job_button_text" />

        <Button
            android:id="@+id/cancel_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="40dp"
            android:onClick="cancelAllJobs"
            android:text="@string/cancel_all_jobs_button_text" />
    </LinearLayout>
</ScrollView>

Service

The Below service is used to do the work and update the result back in MainActivity’s UI.


package com.coderzheaven.android.jobscheduler.service;

import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;

import static com.example.android.jobscheduler.MainActivity.JOB_STARTED;
import static com.example.android.jobscheduler.MainActivity.JOB_STOPPED;
import static com.example.android.jobscheduler.MainActivity.MESSENGER_INTENT_KEY;
import static com.example.android.jobscheduler.MainActivity.WORK_DURATION_KEY;

/**
 * Service to handle callbacks from the JobScheduler. Requests scheduled with the JobScheduler
 * ultimately land on this service's "onStartJob" method. It runs jobs for a specific amount of time
 * and finishes them. It keeps the activity updated with changes via a Messenger.
 */
public class MyJobService extends JobService {

    private static final String TAG = MyJobService.class.getSimpleName();

    private Messenger mActivityMessenger;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "Service created");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "Service destroyed");
    }

    /**
     * When the app's MainActivity is created, it starts this service. This is so that the
     * activity and this service can communicate back and forth. See "setUiCallback()"
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mActivityMessenger = intent.getParcelableExtra(MESSENGER_INTENT_KEY);
        return START_NOT_STICKY;
    }

    @Override
    public boolean onStartJob(final JobParameters params) {
        // The work that this service "does" is simply wait for a certain duration and finish
        // the job (on another thread).

        sendMessage(JOB_STARTED, params.getJobId());

        long duration = params.getExtras().getLong(WORK_DURATION_KEY);

        // Uses a handler to delay the execution of jobFinished().
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                sendMessage(JOB_STOPPED, params.getJobId());
                jobFinished(params, false);
            }
        }, duration);
        Log.i(TAG, "on start job: " + params.getJobId());

        // Return true as there's more work to be done with this job.
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        // Stop tracking these job parameters, as we've 'finished' executing.
        sendMessage(JOB_STOPPED, params.getJobId());
        Log.i(TAG, "on stop job: " + params.getJobId());

        // Return false to drop the job.
        return false;
    }

    private void sendMessage(int messageID, @Nullable Object params) {
        // If this service is launched by the JobScheduler, there's no callback Messenger. It
        // only exists when the MainActivity calls startService() with the callback in the Intent.
        if (mActivityMessenger == null) {
            Log.d(TAG, "Service is bound, not started. There's no callback to send a message to.");
            return;
        }
        Message m = Message.obtain();
        m.what = messageID;
        m.obj = params;
        try {
            mActivityMessenger.send(m);
        } catch (RemoteException e) {
            Log.e(TAG, "Error passing service object back to activity.");
        }
    }
}

All Done. Now run the app and see the result.
You can set the scheduling time in the UI.

Leave a Reply

Your email address will not be published. Required fields are marked *