Android Job Scheduler Example

For scheduling background tasks, Android provides JobScheduler. It allows you to specify conditions for running work. Job schedule is part of android platform and maintains background jobs of all application installed on the device at one place allowing for optimal usage of device resources. JobSchedule can be used to run short and long running tasks such as downloading data, cleaning cache, processing of data, etc.

Now let’s see how you can create and schedule a job using JobScheduler.

JobService

JobService is an android service component with callback methods which the JobSchedule calls when a job needs to be run. That means your background job code needs to be added to callback methods of JobService. To create JobService, you need to extend JobService class and implement its methods onStartJob and onStopJob.

These callback methods are passed JobParameters as an argument by the JobScheduler. JobParameters contains parameters that are set when job is scheduled. These call back methods return boolean value, false indicates that the job is complete and true tells that job is still running in the background, in this case you need to call jobFinished method to inform job scheduler about the finished state after the job is complete.

Since job service is an android component, it runs on main thread. That’s why the tasks which are in job service callback methods needs to be run on background thread.

public class DownloadJobService extends JobService{ @Override public boolean onStartJob(JobParameters jobParameters) { downloadFile(); return false; } @Override public boolean onStopJob(JobParameters jobParameters) { return false; } }

Since it is a service, you need to define it in the manifest xml file and it needs to be protected with android.permission.BIND_JOB_SERVICE permission.

<service android:name=".DbUpdateJobService" android:permission="android.permission.BIND_JOB_SERVICE" ></service>

Scheduling Job

To schedule a job, first you need to get JobScheduler instance by calling getSystemService on the context object passing JOB_SCHEDULER_SERVICE argument to it.

JobScheduler jobScheduler = (JobScheduler)getApplicationContext() .getSystemService(JOB_SCHEDULER_SERVICE);

Then you need to instantiate ComponentName object by passing context and job service class that you created.

ComponentName componentName = new ComponentName(this, DownloadJobService.class);

Then you can create JobInfo object using JobInfo.Builder and setting various configuration parameters. JobInfo.Builder has various setter methods which allow you to define your Job. See the following sections for more details.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setPeriodic(50000).setRequiresBatteryNotLow(true).build();

Then finally, call schedule() on job scheduler object passing job info object to it.

jobScheduler.schedule(jobInfo);

Creating JobInfo

As shown above JobInfo is created using JobInfo.Builder class by using its setter methods, following are the details.

To create a job that runs every time after elapsing of a specified time, you can use setPeriodic method passing time in milliseconds.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setPeriodic(50000).build();

To create a job that needs to be rerun if it fails, you need to use setBackOffCriteria() method passing time interval for the first time retry and retry policy which is used to calculate time interval for retries after first retry.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setBackoffCriteria(6000, JobInfo.BACKOFF_POLICY_LINEAR).build();

You can make a job delayed by specified amount of time by setting minimum latency and you can also specify maximum delay by calling setOverrideDeadline. These two setting are applicable for only non-periodic jobs.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setMinimumLatency(500).setOverrideDeadline(300).build();

To create a job that stays as scheduled even after device reboots, you need call setPersisted() method on JobInfo.Builder passing true as value to it. This setting requires RECEIVE_BOOT_COMPLETED permission.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setPersisted(true).build()

If your job needs network connection and you want to run the job when network of specified kind is used, then you need to specify required network type by calling setRequiredNetworkType() method passing network type.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED).build();

You can configure your job to run when the device is charging and idle by calling setRequiresCharging and setRequiresDeviceIdle methods respectively or you can ask the scheduler to not run the job when battery is low by calling setRequiresBatteryNotLow() method and passing true.

Similarly, you can schedule a job to run only when storage is not low by calling setRequiresStorageNotLow() method and setting it to true.

JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setRequiresCharging(true) .setRequiresDeviceIdle(true) .setRequiresBatteryNotLow(true).build();

To pass data to JobService, you need to set extras by creating PersistableBundle object. JobScheduler passes PersistableBundle object to JobService call back methods.

PersistableBundle pb = new PersistableBundle(); pb.putBoolean("cashback" , false); pb.putDouble("min", 20.3); pb.putString("exclude", "deals"); JobInfo jobInfoObj = new JobInfo.Builder(1, componentName) .setExtras(pb).build();

JobScheduler Example

I’ll show you how to use JobScheduler in android app with an example. The example job reads latest data from Firebase firestore database, updates device local database with new data and deletes expired data from local database. For local database access, Room persistency library is used.

JobService

import android.app.job.JobParameters; import android.app.job.JobService; public class DbUpdateJobService extends JobService{ @Override public boolean onStartJob(JobParameters jobParameters) { FirebaseDbToRoomDataUpdateTask dbUpdateTask = new FirebaseDbToRoomDataUpdateTask(); dbUpdateTask.getCouponsFromFirebaseUpdateLocalDb(this); return false; } @Override public boolean onStopJob(JobParameters jobParameters) { return false; } }

Activity

import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; public class JobSchedulerActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_job); scheduleJob(); } private void scheduleJob(){ SharedPreferences preferences = PreferenceManager. getDefaultSharedPreferences(this); if(!preferences.getBoolean("firstRunComplete", false)){ //schedule the job only once. scheduleJobFirebaseToRoomDataUpdate(); //update shared preference SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("firstRunComplete", true); editor.commit(); } } private void scheduleJobFirebaseToRoomDataUpdate(){ JobScheduler jobScheduler = (JobScheduler)getApplicationContext() .getSystemService(JOB_SCHEDULER_SERVICE); ComponentName componentName = new ComponentName(this, DbUpdateJobService.class); JobInfo jobInfo = new JobInfo.Builder(1, componentName) .setPeriodic(86400000).setRequiredNetworkType( JobInfo.NETWORK_TYPE_NOT_ROAMING) .setPersisted(true).build(); jobScheduler.schedule(jobInfo); } }

FirebaseDbToRoomDataUpdateTask

import android.arch.persistence.room.Room; import android.content.Context; import android.support.annotation.NonNull; import android.util.Log; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.QuerySnapshot; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.concurrent.Executor; public class FirebaseDbToRoomDataUpdateTask { private CouponsDb couponsDb; private FirebaseFirestore firestoreDB; private TaskExecutor taskExecutor; public FirebaseDbToRoomDataUpdateTask(){ firestoreDB = FirebaseFirestore.getInstance(); taskExecutor = new TaskExecutor(); } public void getCouponsFromFirebaseUpdateLocalDb(final Context ctx) { firestoreDB.collection("coupons") .whereEqualTo("createDt", getTodaysDate()) .get() .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() { @Override public void onComplete(@NonNull Task<QuerySnapshot> task) { if (task.isSuccessful()) { List<Coupon> cpnList = task.getResult().toObjects(Coupon.class); Log.d("FIREBASE", "no of coupons "+cpnList.size()); //Run updating local database on worker thread. taskExecutor.execute(new RoomUpdateTask(cpnList, ctx)); } else { Log.d("FIREBASE", "Error getting documents: ", task.getException()); } } }); } public class TaskExecutor implements Executor { @Override public void execute(@NonNull Runnable runnable) { Thread t = new Thread(runnable); t.start(); } } public class RoomUpdateTask implements Runnable{ private List<Coupon> cpnList; private Context context; public RoomUpdateTask(List<Coupon> cpnListIn, Context ctx){ cpnList = cpnListIn; context = ctx; } @Override public void run() { insertLatestCouponsIntoLocalDb(cpnList, context); } } private void insertLatestCouponsIntoLocalDb(List<Coupon> cpns, Context ctx){ couponsDb = Room.databaseBuilder(ctx, CouponsDb.class, "coupons db").build(); //insert new coupons couponsDb.CouponsDb().insertCoupons(cpns); //delete expired coupons couponsDb.CouponsDb().deleteCoupons(getTodaysDate()); Log.d("ROOM", "local database update complete"); Log.d("ROOM", "number of local records " + couponsDb.CouponsDb().getCoupons().size()); } private String getTodaysDate(){ Date date = Calendar.getInstance().getTime(); SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy"); return sdf.format(date); } }

Entity

import android.arch.persistence.room.Entity; import android.arch.persistence.room.PrimaryKey; @Entity public class Coupon { @PrimaryKey(autoGenerate = true) private int couponId; private String store; private String coupon; private String expiryDt; private String createDt; public int getCouponId() { return couponId; } public void setCouponId(int couponId) { this.couponId = couponId; } public String getStore() { return store; } public void setStore(String store) { this.store = store; } public String getCoupon() { return coupon; } public void setCoupon(String coupon) { this.coupon = coupon; } public String getExpiryDt() { return expiryDt; } public void setExpiryDt(String expiryDt) { this.expiryDt = expiryDt; } public String getCreateDt() { return createDt; } public void setCreateDt(String createDt) { this.createDt = createDt; } }

DAO

import android.arch.persistence.room.Dao; import android.arch.persistence.room.Insert; import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; import java.util.List; @Dao public interface CouponsDAO { @Insert(onConflict = OnConflictStrategy.REPLACE) public void insertCoupons(List<Coupon> coupons); @Query("DELETE FROM coupon where expiryDt = :expiryDtIn") public void deleteCoupons(String expiryDtIn); @Query("SELECT * FROM coupon") public List<Coupon> getCoupons(); }

Room database