diff --git a/jobqueue/README.md b/jobqueue/README.md new file mode 100644 index 0000000000..e996ddac75 --- /dev/null +++ b/jobqueue/README.md @@ -0,0 +1,238 @@ +# JobManager + +An Android library that facilitates scheduling persistent jobs which are executed when their +prerequisites have been met. Similar to Path's android-priority-queue. + +## The JobManager Way + +Android apps often need to perform blocking operations. A messaging app might need to make REST +API calls over a network, send SMS messages, download attachments, and interact with a database. + +The standard Android way to do these things are with Services, AsyncTasks, or a dedicated Thread. +However, some of an app's operations might need to wait until certain dependencies are available +(such as a network connection), and some of the operations might need to be durable (complete even if the +app restarts before they have a chance to run). Doing that standard Android way can result in +a lot of retry logic, timers for monitoring dependencies, and one-off code for making operations +durable. + +By contrast, the JobManager way allows operations to be broken up into Jobs. A Job represents a +unit of work to be done, the prerequisites that need to be met (such as network access) before the +work can execute, and the characteristics of the job (such as durable persistence). + +Applications construct a `JobManager` at initialization time: + +``` +public class ApplicationContext extends Application { + + private JobManager jobManager; + + @Override + public void onCreate() { + initializeJobManager(); + } + + private void initializeJobManager() { + this.jobManager = JobManager.newBuilder(this) + .withName("SampleJobManager") + .withConsumerThreads(5) + .build(); + } + + ... + +} +``` + +This constructs a new `JobManager` with 5 consumer threads dedicated to executing Jobs. A +Job looks like this: + +``` +public class SampleJob extends Job { + + public SampleJob() { + super(JobParameters.newBuilder().create()); + } + + @Override + public onAdded() { + // Called after the Job has been added to the queue. + } + + @Override + public void onRun() { + // Here's where we execute our work. + Log.w("SampleJob", "Hello, world!"); + } + + @Override + public void onCanceled() { + // This would be called if the job had failed. + } + + @Override + public boolean onShouldRetry(Exception exception) { + // Called if onRun() had thrown an exception to determine whether + // onRun() should be called again. + return false; + } +} +`` + +A Job is scheduled simply by adding it to the JobManager: + +``` + this.jobManager.add(new SampleJob()); +``` + +## Persistence + +To create durable Jobs, the JobManager needs to be given an interface responsible for serializing +and deserializing Job objects. A `JavaJobSerializer` is included with JobManager that uses Java +Serialization, but you can specify your own serializer if you wish: + +``` +public class ApplicationContext extends Application { + + private JobManager jobManager; + + @Override + public void onCreate() { + initializeJobManager(); + } + + private void initializeJobManager() { + this.jobManager = JobManager.newBuilder(this) + .withName("SampleJobManager") + .withConsumerThreads(5) + .withJobSerializer(new JavaJobSerializer()) + .build(); + } + + ... + +} + +``` + +The Job itself simply needs to declare itself as durable when constructed: + +``` +public class SampleJob extends Job { + + public SampleJob() { + super(JobParameters.newBuilder() + .withPersistence() + .create()); + } + + ... + +``` + +Persistent jobs that are enqueued will be serialized to disk to ensure that they run even if +the App restarts first. A Job's onAdded() method is called after the commit to disk is complete. + +## Requirements + +A Job might have certain requirements that need to be met before it can run. A requirement is +represented by the `Requirement` class. Each `Requirement` must also have a corresponding +`RequirementProvider` that is registered with the JobManager. + +A `Requirement` tells you whether it is present when queried, while a `RequirementProvider` +broadcasts to a listener when a Requirement's status might have changed. `Requirement` is attached +to Job, while `RequirementProvider` is attached to JobManager. + + +One common `Requirement` a `Job` might depend on is the presence of network connectivity. +A `NetworkRequirement` is bundled with JobManager: + +``` +public class ApplicationContext extends Application { + + private JobManager jobManager; + + @Override + public void onCreate() { + initializeJobManager(); + } + + private void initializeJobManager() { + this.jobManager = JobManager.newBuilder(this) + .withName("SampleJobManager") + .withConsumerThreads(5) + .withJobSerializer(new JavaJobSerializer()) + .withRequirementProviders(new NetworkRequirementProvider(this)) + .build(); + } + + ... + +} +``` + +The Job itself simply needs to declare itself as having a `Requirement` when constructed: + +``` +public class SampleJob extends Job { + + public SampleJob(Context context) { + super(JobParameters.newBuilder() + .withPersistence() + .withRequirement(new NetworkRequirement(context)) + .create()); + } + + ... + +``` + +## Dependency Injection + +It is possible that Jobs (and Requirements) might require dependency injection. A simple example +is `Context`, which many Jobs might require, but can't be persisted to disk for durable Jobs. Or +maybe Jobs require more complex DI through libraries such as Dagger. + +JobManager has an extremely primitive DI mechanism strictly for injecting `Context` objects into +Jobs and Requirements after they're deserialized, and includes support for plugging in more complex +DI systems such as Dagger. + +The JobManager `Context` injection works by having your `Job` and/or `Requirement` implement the +`ContextDependent` interface. `Job`s and `Requirement`s implementing that interface will get a +`setContext(Context context)` call immediately after the persistent `Job` or `Requirement` is +deserialized. + +To plugin a more complex DI mechanism, simply pass an instance of the `DependencyInjector` interface + to the `JobManager`: + +``` +public class ApplicationContext extends Application implements DependencyInjector { + + private JobManager jobManager; + + @Override + public void onCreate() { + initializeJobManager(); + } + + private void initializeJobManager() { + this.jobManager = JobManager.newBuilder(this) + .withName("SampleJobManager") + .withConsumerThreads(5) + .withJobSerializer(new JavaJobSerializer()) + .withRequirementProviders(new NetworkRequirementProvider(this)) + .withDependencyInjector(this) + .build(); + } + + @Override + public void injectDependencies(Object object) { + // And here we do our DI magic. + } + + ... + +} +``` + +`injectDependencies(Object object)` will be called for a `Job` before the job's `onAdded()` method +is called, or after a persistent job is deserialized. \ No newline at end of file