mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 02:55:23 +00:00
Persistent job queue, derivative of android-priority-jobqueue.
// FREEBIE
This commit is contained in:
parent
20cf775b1e
commit
544f06451f
1
jobqueue/.gitignore
vendored
Normal file
1
jobqueue/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
19
jobqueue/build.gradle
Normal file
19
jobqueue/build.gradle
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 20
|
||||||
|
buildToolsVersion "20.0.0"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "org.whispersystems.jobqueue"
|
||||||
|
minSdkVersion 9
|
||||||
|
targetSdkVersion 19
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.jobs.PersistentTestJob;
|
||||||
|
import org.whispersystems.jobqueue.jobs.RequirementTestJob;
|
||||||
|
import org.whispersystems.jobqueue.jobs.TestJob;
|
||||||
|
import org.whispersystems.jobqueue.persistence.JavaJobSerializer;
|
||||||
|
import org.whispersystems.jobqueue.util.MockRequirement;
|
||||||
|
import org.whispersystems.jobqueue.util.MockRequirementProvider;
|
||||||
|
import org.whispersystems.jobqueue.util.PersistentMockRequirement;
|
||||||
|
import org.whispersystems.jobqueue.util.PersistentRequirement;
|
||||||
|
import org.whispersystems.jobqueue.util.PersistentResult;
|
||||||
|
|
||||||
|
public class JobManagerTest extends AndroidTestCase {
|
||||||
|
|
||||||
|
public void testTransientJobExecution() throws InterruptedException {
|
||||||
|
TestJob testJob = new TestJob();
|
||||||
|
JobManager jobManager = new JobManager(getContext(), "transient-test", null, null, 1);
|
||||||
|
|
||||||
|
jobManager.add(testJob);
|
||||||
|
|
||||||
|
assertTrue(testJob.isAdded());
|
||||||
|
assertTrue(testJob.isRan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTransientRequirementJobExecution() throws InterruptedException {
|
||||||
|
MockRequirementProvider provider = new MockRequirementProvider();
|
||||||
|
MockRequirement requirement = new MockRequirement(false);
|
||||||
|
TestJob testJob = new RequirementTestJob(requirement);
|
||||||
|
JobManager jobManager = new JobManager(getContext(), "transient-requirement-test", provider, null, 1);
|
||||||
|
|
||||||
|
jobManager.add(testJob);
|
||||||
|
|
||||||
|
assertTrue(testJob.isAdded());
|
||||||
|
assertTrue(!testJob.isRan());
|
||||||
|
|
||||||
|
requirement.setPresent(true);
|
||||||
|
provider.fireChange();
|
||||||
|
|
||||||
|
assertTrue(testJob.isRan());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPersistentJobExecuton() throws InterruptedException {
|
||||||
|
PersistentMockRequirement requirement = new PersistentMockRequirement();
|
||||||
|
PersistentTestJob testJob = new PersistentTestJob(requirement);
|
||||||
|
JobManager jobManager = new JobManager(getContext(), "persistent-requirement-test3", null, new JavaJobSerializer(getContext()), 1);
|
||||||
|
|
||||||
|
PersistentResult.getInstance().reset();
|
||||||
|
PersistentRequirement.getInstance().setPresent(false);
|
||||||
|
|
||||||
|
jobManager.add(testJob);
|
||||||
|
|
||||||
|
assertTrue(PersistentResult.getInstance().isAdded());
|
||||||
|
assertTrue(!PersistentResult.getInstance().isRan());
|
||||||
|
|
||||||
|
PersistentRequirement.getInstance().setPresent(true);
|
||||||
|
jobManager = new JobManager(getContext(), "persistent-requirement-test3", null, new JavaJobSerializer(getContext()), 1);
|
||||||
|
|
||||||
|
assertTrue(PersistentResult.getInstance().isRan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testEncryptedJobExecuton() throws InterruptedException {
|
||||||
|
EncryptionKeys keys = new EncryptionKeys("foobar");
|
||||||
|
PersistentMockRequirement requirement = new PersistentMockRequirement();
|
||||||
|
PersistentTestJob testJob = new PersistentTestJob(requirement, keys);
|
||||||
|
JobManager jobManager = new JobManager(getContext(), "persistent-requirement-test4", null, new JavaJobSerializer(getContext()), 1);
|
||||||
|
jobManager.setEncryptionKeys(keys);
|
||||||
|
|
||||||
|
PersistentResult.getInstance().reset();
|
||||||
|
PersistentRequirement.getInstance().setPresent(false);
|
||||||
|
|
||||||
|
jobManager.add(testJob);
|
||||||
|
|
||||||
|
assertTrue(PersistentResult.getInstance().isAdded());
|
||||||
|
assertTrue(!PersistentResult.getInstance().isRan());
|
||||||
|
|
||||||
|
PersistentRequirement.getInstance().setPresent(true);
|
||||||
|
jobManager = new JobManager(getContext(), "persistent-requirement-test4", null, new JavaJobSerializer(getContext()), 1);
|
||||||
|
|
||||||
|
assertTrue(!PersistentResult.getInstance().isRan());
|
||||||
|
|
||||||
|
jobManager.setEncryptionKeys(keys);
|
||||||
|
|
||||||
|
assertTrue(PersistentResult.getInstance().isRan());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.whispersystems.jobqueue.jobs;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.EncryptionKeys;
|
||||||
|
import org.whispersystems.jobqueue.Job;
|
||||||
|
import org.whispersystems.jobqueue.JobParameters;
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
import org.whispersystems.jobqueue.util.PersistentResult;
|
||||||
|
|
||||||
|
public class PersistentTestJob extends Job {
|
||||||
|
|
||||||
|
public PersistentTestJob(Requirement requirement) {
|
||||||
|
super(JobParameters.newBuilder().withRequirement(requirement).withPersistence().create());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersistentTestJob(Requirement requirement, EncryptionKeys keys) {
|
||||||
|
super(JobParameters.newBuilder().withRequirement(requirement).withPersistence().withEncryption(keys).create());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdded() {
|
||||||
|
PersistentResult.getInstance().onAdded();;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRun() throws Throwable {
|
||||||
|
PersistentResult.getInstance().onRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCanceled() {
|
||||||
|
PersistentResult.getInstance().onCanceled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onShouldRetry(Throwable throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package org.whispersystems.jobqueue.jobs;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.JobParameters;
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
|
||||||
|
public class RequirementTestJob extends TestJob {
|
||||||
|
|
||||||
|
public RequirementTestJob(Requirement requirement) {
|
||||||
|
super(JobParameters.newBuilder().withRequirement(requirement).create());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package org.whispersystems.jobqueue.jobs;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.Job;
|
||||||
|
import org.whispersystems.jobqueue.JobParameters;
|
||||||
|
|
||||||
|
public class TestJob extends Job {
|
||||||
|
|
||||||
|
private final Object ADDED_LOCK = new Object();
|
||||||
|
private final Object RAN_LOCK = new Object();
|
||||||
|
private final Object CANCELED_LOCK = new Object();
|
||||||
|
|
||||||
|
private boolean added = false;
|
||||||
|
private boolean ran = false;
|
||||||
|
private boolean canceled = false;
|
||||||
|
|
||||||
|
public TestJob() {
|
||||||
|
this(JobParameters.newBuilder().create());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestJob(JobParameters parameters) {
|
||||||
|
super(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdded() {
|
||||||
|
synchronized (ADDED_LOCK) {
|
||||||
|
this.added = true;
|
||||||
|
this.ADDED_LOCK.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRun() throws Throwable {
|
||||||
|
synchronized (RAN_LOCK) {
|
||||||
|
this.ran = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCanceled() {
|
||||||
|
synchronized (CANCELED_LOCK) {
|
||||||
|
this.canceled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onShouldRetry(Throwable throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAdded() throws InterruptedException {
|
||||||
|
synchronized (ADDED_LOCK) {
|
||||||
|
if (!added) ADDED_LOCK.wait(1000);
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRan() throws InterruptedException {
|
||||||
|
synchronized (RAN_LOCK) {
|
||||||
|
if (!ran) RAN_LOCK.wait(1000);
|
||||||
|
return ran;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCanceled() throws InterruptedException {
|
||||||
|
synchronized (CANCELED_LOCK) {
|
||||||
|
if (!canceled) CANCELED_LOCK.wait(1000);
|
||||||
|
return canceled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.whispersystems.jobqueue.util;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class MockRequirement implements Requirement {
|
||||||
|
|
||||||
|
private AtomicBoolean present;
|
||||||
|
|
||||||
|
public MockRequirement(boolean present) {
|
||||||
|
this.present = new AtomicBoolean(present);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPresent(boolean present) {
|
||||||
|
this.present.set(present);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return present.get();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.whispersystems.jobqueue.util;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.requirements.RequirementListener;
|
||||||
|
import org.whispersystems.jobqueue.requirements.RequirementProvider;
|
||||||
|
|
||||||
|
public class MockRequirementProvider implements RequirementProvider {
|
||||||
|
|
||||||
|
private RequirementListener listener;
|
||||||
|
|
||||||
|
public void fireChange() {
|
||||||
|
listener.onRequirementStatusChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setListener(RequirementListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package org.whispersystems.jobqueue.util;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
|
||||||
|
public class PersistentMockRequirement implements Requirement {
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return PersistentRequirement.getInstance().isPresent();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.whispersystems.jobqueue.util;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class PersistentRequirement {
|
||||||
|
|
||||||
|
private AtomicBoolean present = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private static final PersistentRequirement instance = new PersistentRequirement();
|
||||||
|
|
||||||
|
public static PersistentRequirement getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPresent(boolean present) {
|
||||||
|
this.present.set(present);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPresent() {
|
||||||
|
return present.get();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package org.whispersystems.jobqueue.util;
|
||||||
|
|
||||||
|
public class PersistentResult {
|
||||||
|
|
||||||
|
private final Object ADDED_LOCK = new Object();
|
||||||
|
private final Object RAN_LOCK = new Object();
|
||||||
|
private final Object CANCELED_LOCK = new Object();
|
||||||
|
|
||||||
|
private boolean added = false;
|
||||||
|
private boolean ran = false;
|
||||||
|
private boolean canceled = false;
|
||||||
|
|
||||||
|
private static final PersistentResult instance = new PersistentResult();
|
||||||
|
|
||||||
|
public static PersistentResult getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onAdded() {
|
||||||
|
synchronized (ADDED_LOCK) {
|
||||||
|
this.added = true;
|
||||||
|
this.ADDED_LOCK.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRun() throws Throwable {
|
||||||
|
synchronized (RAN_LOCK) {
|
||||||
|
this.ran = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCanceled() {
|
||||||
|
synchronized (CANCELED_LOCK) {
|
||||||
|
this.canceled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAdded() throws InterruptedException {
|
||||||
|
synchronized (ADDED_LOCK) {
|
||||||
|
if (!added) ADDED_LOCK.wait(1000);
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRan() throws InterruptedException {
|
||||||
|
synchronized (RAN_LOCK) {
|
||||||
|
if (!ran) RAN_LOCK.wait(1000);
|
||||||
|
return ran;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCanceled() throws InterruptedException {
|
||||||
|
synchronized (CANCELED_LOCK) {
|
||||||
|
if (!canceled) CANCELED_LOCK.wait(1000);
|
||||||
|
return canceled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
synchronized (ADDED_LOCK) {
|
||||||
|
this.added = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (RAN_LOCK) {
|
||||||
|
this.ran = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (CANCELED_LOCK) {
|
||||||
|
this.canceled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
jobqueue/src/main/AndroidManifest.xml
Normal file
6
jobqueue/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.whispersystems.jobqueue">
|
||||||
|
|
||||||
|
<application />
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,14 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
public class EncryptionKeys {
|
||||||
|
|
||||||
|
private transient final String keys;
|
||||||
|
|
||||||
|
public EncryptionKeys(String keys) {
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
60
jobqueue/src/main/java/org/whispersystems/jobqueue/Job.java
Normal file
60
jobqueue/src/main/java/org/whispersystems/jobqueue/Job.java
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class Job implements Serializable {
|
||||||
|
|
||||||
|
private final JobParameters parameters;
|
||||||
|
|
||||||
|
private transient long persistentId;
|
||||||
|
|
||||||
|
public Job(JobParameters parameters) {
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Requirement> getRequirements() {
|
||||||
|
return parameters.getRequirements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRequirementsMet() {
|
||||||
|
for (Requirement requirement : parameters.getRequirements()) {
|
||||||
|
if (!requirement.isPresent()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPersistent() {
|
||||||
|
return parameters.isPersistent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EncryptionKeys getEncryptionKeys() {
|
||||||
|
return parameters.getEncryptionKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncryptionKeys(EncryptionKeys keys) {
|
||||||
|
parameters.setEncryptionKeys(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRetryCount() {
|
||||||
|
return parameters.getRetryCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPersistentId(long persistentId) {
|
||||||
|
this.persistentId = persistentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getPersistentId() {
|
||||||
|
return persistentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void onAdded();
|
||||||
|
public abstract void onRun() throws Throwable;
|
||||||
|
public abstract void onCanceled();
|
||||||
|
public abstract boolean onShouldRetry(Throwable throwable);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.persistence.PersistentStorage;
|
||||||
|
|
||||||
|
public class JobConsumer extends Thread {
|
||||||
|
|
||||||
|
private final JobQueue jobQueue;
|
||||||
|
private final PersistentStorage persistentStorage;
|
||||||
|
|
||||||
|
public JobConsumer(String name, JobQueue jobQueue, PersistentStorage persistentStorage) {
|
||||||
|
super(name);
|
||||||
|
this.jobQueue = jobQueue;
|
||||||
|
this.persistentStorage = persistentStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
Job job = jobQueue.getNext();
|
||||||
|
|
||||||
|
if (!runJob(job)) {
|
||||||
|
job.onCanceled();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.isPersistent()) {
|
||||||
|
persistentStorage.remove(job.getPersistentId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean runJob(Job job) {
|
||||||
|
int retryCount = job.getRetryCount();
|
||||||
|
|
||||||
|
for (int i=retryCount;i>0;i--) {
|
||||||
|
try {
|
||||||
|
job.onRun();
|
||||||
|
return true;
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
if (!job.onShouldRetry(throwable)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.persistence.JobSerializer;
|
||||||
|
import org.whispersystems.jobqueue.persistence.PersistentStorage;
|
||||||
|
import org.whispersystems.jobqueue.requirements.RequirementListener;
|
||||||
|
import org.whispersystems.jobqueue.requirements.RequirementProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class JobManager implements RequirementListener {
|
||||||
|
|
||||||
|
private final JobQueue jobQueue = new JobQueue();
|
||||||
|
private final Executor eventExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private final PersistentStorage persistentStorage;
|
||||||
|
|
||||||
|
public JobManager(Context context, String name,
|
||||||
|
RequirementProvider requirementProvider,
|
||||||
|
JobSerializer jobSerializer, int consumers)
|
||||||
|
{
|
||||||
|
this.persistentStorage = new PersistentStorage(context, name, jobSerializer);
|
||||||
|
eventExecutor.execute(new LoadTask(null));
|
||||||
|
|
||||||
|
if (requirementProvider != null) {
|
||||||
|
requirementProvider.setListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i=0;i<consumers;i++) {
|
||||||
|
new JobConsumer("JobConsumer-" + i, jobQueue, persistentStorage).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncryptionKeys(EncryptionKeys keys) {
|
||||||
|
if (hasLoadedEncrypted.compareAndSet(false, true)) {
|
||||||
|
eventExecutor.execute(new LoadTask(keys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(final Job job) {
|
||||||
|
eventExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
if (job.isPersistent()) {
|
||||||
|
persistentStorage.store(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
job.onAdded();
|
||||||
|
jobQueue.add(job);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w("JobManager", e);
|
||||||
|
job.onCanceled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequirementStatusChanged() {
|
||||||
|
eventExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
jobQueue.onRequirementStatusChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoadTask implements Runnable {
|
||||||
|
|
||||||
|
private final EncryptionKeys keys;
|
||||||
|
|
||||||
|
public LoadTask(EncryptionKeys keys) {
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
List<Job> pendingJobs;
|
||||||
|
|
||||||
|
if (keys == null) pendingJobs = persistentStorage.getAllUnencrypted();
|
||||||
|
else pendingJobs = persistentStorage.getAllEncrypted(keys);
|
||||||
|
|
||||||
|
jobQueue.addAll(pendingJobs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class JobParameters implements Serializable {
|
||||||
|
|
||||||
|
private transient EncryptionKeys encryptionKeys;
|
||||||
|
|
||||||
|
private final List<Requirement> requirements;
|
||||||
|
private final boolean isPersistent;
|
||||||
|
private final int retryCount;
|
||||||
|
|
||||||
|
private JobParameters(List<Requirement> requirements,
|
||||||
|
boolean isPersistent,
|
||||||
|
EncryptionKeys encryptionKeys,
|
||||||
|
int retryCount)
|
||||||
|
{
|
||||||
|
this.requirements = requirements;
|
||||||
|
this.isPersistent = isPersistent;
|
||||||
|
this.encryptionKeys = encryptionKeys;
|
||||||
|
this.retryCount = retryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Requirement> getRequirements() {
|
||||||
|
return requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPersistent() {
|
||||||
|
return isPersistent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EncryptionKeys getEncryptionKeys() {
|
||||||
|
return encryptionKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncryptionKeys(EncryptionKeys encryptionKeys) {
|
||||||
|
this.encryptionKeys = encryptionKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRetryCount() {
|
||||||
|
return retryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private List<Requirement> requirements = new LinkedList<>();
|
||||||
|
private boolean isPersistent = false;
|
||||||
|
private EncryptionKeys encryptionKeys = null;
|
||||||
|
private int retryCount = 100;
|
||||||
|
|
||||||
|
public Builder withRequirement(Requirement requirement) {
|
||||||
|
this.requirements.add(requirement);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withPersistence() {
|
||||||
|
this.isPersistent = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withEncryption(EncryptionKeys encryptionKeys) {
|
||||||
|
this.encryptionKeys = encryptionKeys;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withRetryCount(int retryCount) {
|
||||||
|
this.retryCount = retryCount;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobParameters create() {
|
||||||
|
return new JobParameters(requirements, isPersistent, encryptionKeys, retryCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.whispersystems.jobqueue;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
|
||||||
|
public class JobQueue {
|
||||||
|
|
||||||
|
private final LinkedList<Job> jobQueue = new LinkedList<>();
|
||||||
|
|
||||||
|
public synchronized void onRequirementStatusChanged() {
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void add(Job job) {
|
||||||
|
jobQueue.add(job);
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addAll(List<Job> jobs) {
|
||||||
|
jobQueue.addAll(jobs);
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Job getNext() {
|
||||||
|
try {
|
||||||
|
Job nextAvailableJob;
|
||||||
|
|
||||||
|
while ((nextAvailableJob = getNextAvailableJob()) == null) {
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextAvailableJob;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Job getNextAvailableJob() {
|
||||||
|
if (jobQueue.isEmpty()) return null;
|
||||||
|
|
||||||
|
ListIterator<Job> iterator = jobQueue.listIterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Job job = iterator.next();
|
||||||
|
|
||||||
|
if (job.isRequirementsMet()) {
|
||||||
|
iterator.remove();
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.jobqueue.dependencies;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public interface ContextDependent {
|
||||||
|
public void setContext(Context context);
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package org.whispersystems.jobqueue.persistence;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.EncryptionKeys;
|
||||||
|
import org.whispersystems.jobqueue.Job;
|
||||||
|
import org.whispersystems.jobqueue.dependencies.ContextDependent;
|
||||||
|
import org.whispersystems.jobqueue.requirements.Requirement;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
public class JavaJobSerializer implements JobSerializer {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public JavaJobSerializer(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String serialize(Job job) throws IOException {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||||
|
oos.writeObject(job);
|
||||||
|
|
||||||
|
return Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Job deserialize(EncryptionKeys keys, boolean encrypted, String serialized) throws IOException {
|
||||||
|
try {
|
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(serialized, Base64.NO_WRAP));
|
||||||
|
ObjectInputStream ois = new ObjectInputStream(bais);
|
||||||
|
|
||||||
|
Job job = (Job)ois.readObject();
|
||||||
|
|
||||||
|
if (job instanceof ContextDependent) {
|
||||||
|
((ContextDependent)job).setContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Requirement requirement : job.getRequirements()) {
|
||||||
|
if (requirement instanceof ContextDependent) {
|
||||||
|
((ContextDependent)requirement).setContext(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
job.setEncryptionKeys(keys);
|
||||||
|
|
||||||
|
return job;
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package org.whispersystems.jobqueue.persistence;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.EncryptionKeys;
|
||||||
|
import org.whispersystems.jobqueue.Job;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface JobSerializer {
|
||||||
|
|
||||||
|
public String serialize(Job job) throws IOException;
|
||||||
|
public Job deserialize(EncryptionKeys keys, boolean encrypted, String serialized) throws IOException;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
package org.whispersystems.jobqueue.persistence;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.EncryptionKeys;
|
||||||
|
import org.whispersystems.jobqueue.Job;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PersistentStorage {
|
||||||
|
|
||||||
|
private static final int DATABASE_VERSION = 1;
|
||||||
|
|
||||||
|
private static final String TABLE_NAME = "queue";
|
||||||
|
private static final String ID = "_id";
|
||||||
|
private static final String ITEM = "item";
|
||||||
|
private static final String ENCRYPTED = "encrypted";
|
||||||
|
|
||||||
|
private static final String DATABASE_CREATE = String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY, %s TEXT NOT NULL, %s INTEGER DEFAULT 0);",
|
||||||
|
TABLE_NAME, ID, ITEM, ENCRYPTED);
|
||||||
|
|
||||||
|
private final DatabaseHelper databaseHelper;
|
||||||
|
private final JobSerializer jobSerializer;
|
||||||
|
|
||||||
|
public PersistentStorage(Context context, String name, JobSerializer serializer) {
|
||||||
|
this.databaseHelper = new DatabaseHelper(context, "_jobqueue-" + name);
|
||||||
|
this.jobSerializer = serializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void store(Job job) throws IOException {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(ITEM, jobSerializer.serialize(job));
|
||||||
|
contentValues.put(ENCRYPTED, job.getEncryptionKeys() != null);
|
||||||
|
|
||||||
|
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||||
|
job.setPersistentId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public List<Job> getAll(EncryptionKeys keys) {
|
||||||
|
// return getJobs(keys, null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public List<Job> getAllUnencrypted() {
|
||||||
|
return getJobs(null, ENCRYPTED + " = 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Job> getAllEncrypted(EncryptionKeys keys) {
|
||||||
|
return getJobs(keys, ENCRYPTED + " = 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Job> getJobs(EncryptionKeys keys, String where) {
|
||||||
|
List<Job> results = new LinkedList<>();
|
||||||
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
|
Cursor cursor = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
cursor = database.query(TABLE_NAME, null, where, null, null, null, ID + " ASC", null);
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||||
|
String item = cursor.getString(cursor.getColumnIndexOrThrow(ITEM));
|
||||||
|
boolean encrypted = cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTED)) == 1;
|
||||||
|
|
||||||
|
try{
|
||||||
|
Job job = jobSerializer.deserialize(keys, encrypted, item);
|
||||||
|
|
||||||
|
job.setPersistentId(id);
|
||||||
|
results.add(job);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w("PersistentStore", e);
|
||||||
|
remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (cursor != null)
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void remove(long id) {
|
||||||
|
databaseHelper.getWritableDatabase()
|
||||||
|
.delete(TABLE_NAME, ID + " = ?", new String[] {String.valueOf(id)});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DatabaseHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
|
public DatabaseHelper(Context context, String name) {
|
||||||
|
super(context, name, null, DATABASE_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(DATABASE_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.whispersystems.jobqueue.requirements;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
|
||||||
|
import org.whispersystems.jobqueue.dependencies.ContextDependent;
|
||||||
|
|
||||||
|
public class NetworkRequirement implements Requirement, ContextDependent {
|
||||||
|
|
||||||
|
private transient Context context;
|
||||||
|
|
||||||
|
public NetworkRequirement(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkRequirement() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo netInfo = cm.getActiveNetworkInfo();
|
||||||
|
|
||||||
|
return netInfo != null && netInfo.isConnectedOrConnecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContext(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.whispersystems.jobqueue.requirements;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
|
||||||
|
public class NetworkRequirementProvider implements RequirementProvider {
|
||||||
|
|
||||||
|
private RequirementListener listener;
|
||||||
|
|
||||||
|
public NetworkRequirementProvider(Context context) {
|
||||||
|
context.getApplicationContext().registerReceiver(new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (listener == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo netInfo = cm.getActiveNetworkInfo();
|
||||||
|
boolean connected = netInfo != null && netInfo.isConnectedOrConnecting();
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
listener.onRequirementStatusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setListener(RequirementListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.jobqueue.requirements;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public interface Requirement extends Serializable {
|
||||||
|
public boolean isPresent();
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package org.whispersystems.jobqueue.requirements;
|
||||||
|
|
||||||
|
public interface RequirementListener {
|
||||||
|
public void onRequirementStatusChanged();
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package org.whispersystems.jobqueue.requirements;
|
||||||
|
|
||||||
|
public interface RequirementProvider {
|
||||||
|
public void setListener(RequirementListener listener);
|
||||||
|
}
|
2
jobqueue/src/main/res/values/strings.xml
Normal file
2
jobqueue/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<resources>
|
||||||
|
</resources>
|
@ -1 +1 @@
|
|||||||
include ':library', ':libaxolotl'
|
include ':library', ':libaxolotl', ':jobqueue'
|
||||||
|
Loading…
Reference in New Issue
Block a user