diff --git a/jobqueue/.gitignore b/jobqueue/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/jobqueue/.gitignore @@ -0,0 +1 @@ +/build diff --git a/jobqueue/build.gradle b/jobqueue/build.gradle new file mode 100644 index 0000000000..0f3bb93fdf --- /dev/null +++ b/jobqueue/build.gradle @@ -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 + } +} \ No newline at end of file diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/JobManagerTest.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/JobManagerTest.java new file mode 100644 index 0000000000..312de751bc --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/JobManagerTest.java @@ -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()); + } + + +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/PersistentTestJob.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/PersistentTestJob.java new file mode 100644 index 0000000000..0f582e72ef --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/PersistentTestJob.java @@ -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; + } +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/RequirementTestJob.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/RequirementTestJob.java new file mode 100644 index 0000000000..8dac14c608 --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/RequirementTestJob.java @@ -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()); + } + +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/TestJob.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/TestJob.java new file mode 100644 index 0000000000..228d77d6c7 --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/jobs/TestJob.java @@ -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; + } + } +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/MockRequirement.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/MockRequirement.java new file mode 100644 index 0000000000..779d07747e --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/MockRequirement.java @@ -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(); + } +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/MockRequirementProvider.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/MockRequirementProvider.java new file mode 100644 index 0000000000..235449e18e --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/MockRequirementProvider.java @@ -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; + } +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentMockRequirement.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentMockRequirement.java new file mode 100644 index 0000000000..f39eb491a3 --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentMockRequirement.java @@ -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(); + } +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentRequirement.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentRequirement.java new file mode 100644 index 0000000000..917574805a --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentRequirement.java @@ -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(); + } +} diff --git a/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentResult.java b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentResult.java new file mode 100644 index 0000000000..ffd213ae85 --- /dev/null +++ b/jobqueue/src/androidTest/java/org/whispersystems/jobqueue/util/PersistentResult.java @@ -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; + } + } + +} diff --git a/jobqueue/src/main/AndroidManifest.xml b/jobqueue/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4265f4c4fa --- /dev/null +++ b/jobqueue/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/EncryptionKeys.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/EncryptionKeys.java new file mode 100644 index 0000000000..f414543e01 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/EncryptionKeys.java @@ -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; + } +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/Job.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/Job.java new file mode 100644 index 0000000000..ccc27a67c5 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/Job.java @@ -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 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); + + +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/JobConsumer.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobConsumer.java new file mode 100644 index 0000000000..fd187a7fb4 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobConsumer.java @@ -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; + } + +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java new file mode 100644 index 0000000000..0f4aeb0965 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java @@ -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 pendingJobs; + + if (keys == null) pendingJobs = persistentStorage.getAllUnencrypted(); + else pendingJobs = persistentStorage.getAllEncrypted(keys); + + jobQueue.addAll(pendingJobs); + } + } + +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/JobParameters.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobParameters.java new file mode 100644 index 0000000000..2504d14618 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobParameters.java @@ -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 requirements; + private final boolean isPersistent; + private final int retryCount; + + private JobParameters(List requirements, + boolean isPersistent, + EncryptionKeys encryptionKeys, + int retryCount) + { + this.requirements = requirements; + this.isPersistent = isPersistent; + this.encryptionKeys = encryptionKeys; + this.retryCount = retryCount; + } + + public List 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 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); + } + } +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/JobQueue.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobQueue.java new file mode 100644 index 0000000000..2901c900d1 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobQueue.java @@ -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 jobQueue = new LinkedList<>(); + + public synchronized void onRequirementStatusChanged() { + notifyAll(); + } + + public synchronized void add(Job job) { + jobQueue.add(job); + notifyAll(); + } + + public synchronized void addAll(List 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 iterator = jobQueue.listIterator(); + while (iterator.hasNext()) { + Job job = iterator.next(); + + if (job.isRequirementsMet()) { + iterator.remove(); + return job; + } + } + + return null; + } +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/dependencies/ContextDependent.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/dependencies/ContextDependent.java new file mode 100644 index 0000000000..0cdc91abe3 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/dependencies/ContextDependent.java @@ -0,0 +1,7 @@ +package org.whispersystems.jobqueue.dependencies; + +import android.content.Context; + +public interface ContextDependent { + public void setContext(Context context); +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/JavaJobSerializer.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/JavaJobSerializer.java new file mode 100644 index 0000000000..761ef6117d --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/JavaJobSerializer.java @@ -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); + } + } +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/JobSerializer.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/JobSerializer.java new file mode 100644 index 0000000000..dea6cacf3d --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/JobSerializer.java @@ -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; + +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/PersistentStorage.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/PersistentStorage.java new file mode 100644 index 0000000000..9e99882b5e --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/persistence/PersistentStorage.java @@ -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 getAll(EncryptionKeys keys) { +// return getJobs(keys, null); +// } + + public List getAllUnencrypted() { + return getJobs(null, ENCRYPTED + " = 0"); + } + + public List getAllEncrypted(EncryptionKeys keys) { + return getJobs(keys, ENCRYPTED + " = 1"); + } + + private List getJobs(EncryptionKeys keys, String where) { + List 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) { + + } + } + +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirement.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirement.java new file mode 100644 index 0000000000..1f1f7c3be8 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirement.java @@ -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; + } +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java new file mode 100644 index 0000000000..c16863568d --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java @@ -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; + } + +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/Requirement.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/Requirement.java new file mode 100644 index 0000000000..22e7a40bc0 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/Requirement.java @@ -0,0 +1,7 @@ +package org.whispersystems.jobqueue.requirements; + +import java.io.Serializable; + +public interface Requirement extends Serializable { + public boolean isPresent(); +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementListener.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementListener.java new file mode 100644 index 0000000000..34460d6f85 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementListener.java @@ -0,0 +1,5 @@ +package org.whispersystems.jobqueue.requirements; + +public interface RequirementListener { + public void onRequirementStatusChanged(); +} diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java new file mode 100644 index 0000000000..caaddd1972 --- /dev/null +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java @@ -0,0 +1,5 @@ +package org.whispersystems.jobqueue.requirements; + +public interface RequirementProvider { + public void setListener(RequirementListener listener); +} diff --git a/jobqueue/src/main/res/values/strings.xml b/jobqueue/src/main/res/values/strings.xml new file mode 100644 index 0000000000..8542005550 --- /dev/null +++ b/jobqueue/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/settings.gradle b/settings.gradle index 7caf8cfc8f..beb0a7fce8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':library', ':libaxolotl' +include ':library', ':libaxolotl', ':jobqueue'