diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 68b20722a4..e16f58ac54 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -528,6 +528,7 @@
+
0) {
+ return job.getRunIteration() < job.getRetryCount();
+ }
+ return System.currentTimeMillis() < job.getRetryUntil();
+ }
}
diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java b/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java
index 3873c721a8..506747e4bd 100644
--- a/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java
+++ b/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java
@@ -28,26 +28,30 @@ import java.util.concurrent.TimeUnit;
*/
public class JobParameters implements Serializable {
+ private static final long serialVersionUID = 4880456378402584584L;
+
private transient EncryptionKeys encryptionKeys;
private final List requirements;
private final boolean isPersistent;
private final int retryCount;
+ private final long retryUntil;
private final String groupId;
private final boolean wakeLock;
private final long wakeLockTimeout;
private JobParameters(List requirements,
- boolean isPersistent, String groupId,
- EncryptionKeys encryptionKeys,
- int retryCount, boolean wakeLock,
- long wakeLockTimeout)
+ boolean isPersistent, String groupId,
+ EncryptionKeys encryptionKeys,
+ int retryCount, long retryUntil, boolean wakeLock,
+ long wakeLockTimeout)
{
this.requirements = requirements;
this.isPersistent = isPersistent;
this.groupId = groupId;
this.encryptionKeys = encryptionKeys;
this.retryCount = retryCount;
+ this.retryUntil = retryUntil;
this.wakeLock = wakeLock;
this.wakeLockTimeout = wakeLockTimeout;
}
@@ -72,6 +76,10 @@ public class JobParameters implements Serializable {
return retryCount;
}
+ public long getRetryUntil() {
+ return retryUntil;
+ }
+
/**
* @return a builder used to construct JobParameters.
*/
@@ -96,6 +104,7 @@ public class JobParameters implements Serializable {
private boolean isPersistent = false;
private EncryptionKeys encryptionKeys = null;
private int retryCount = 100;
+ private long retryDuration = 0;
private String groupId = null;
private boolean wakeLock = false;
private long wakeLockTimeout = 0;
@@ -139,7 +148,14 @@ public class JobParameters implements Serializable {
* @return the builder.
*/
public Builder withRetryCount(int retryCount) {
- this.retryCount = retryCount;
+ this.retryCount = retryCount;
+ this.retryDuration = 0;
+ return this;
+ }
+
+ public Builder withRetryDuration(long duration) {
+ this.retryDuration = duration;
+ this.retryCount = 0;
return this;
}
@@ -184,7 +200,7 @@ public class JobParameters implements Serializable {
* @return the JobParameters instance that describes a Job.
*/
public JobParameters create() {
- return new JobParameters(requirements, isPersistent, groupId, encryptionKeys, retryCount, wakeLock, wakeLockTimeout);
+ return new JobParameters(requirements, isPersistent, groupId, encryptionKeys, retryCount, System.currentTimeMillis() + retryDuration, wakeLock, wakeLockTimeout);
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobQueue.java b/src/org/thoughtcrime/securesms/jobmanager/JobQueue.java
index 246b3242fa..07d986e69d 100644
--- a/src/org/thoughtcrime/securesms/jobmanager/JobQueue.java
+++ b/src/org/thoughtcrime/securesms/jobmanager/JobQueue.java
@@ -16,16 +16,18 @@
*/
package org.thoughtcrime.securesms.jobmanager;
-import java.util.HashSet;
+import android.support.annotation.NonNull;
+
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
-import java.util.Set;
+import java.util.Map;
class JobQueue {
- private final Set activeGroupIds = new HashSet<>();
- private final LinkedList jobQueue = new LinkedList<>();
+ private final Map activeGroupIds = new HashMap<>();
+ private final LinkedList jobQueue = new LinkedList<>();
synchronized void onRequirementStatusChanged() {
notifyAll();
@@ -33,14 +35,29 @@ class JobQueue {
synchronized void add(Job job) {
jobQueue.add(job);
+ processJobAddition(job);
notifyAll();
}
synchronized void addAll(List jobs) {
jobQueue.addAll(jobs);
+
+ for (Job job : jobs) {
+ processJobAddition(job);
+ }
+
notifyAll();
}
+ private void processJobAddition(@NonNull Job job) {
+ if (isJobActive(job) && isGroupIdAvailable(job)) {
+ setGroupIdUnavailable(job);
+ } else if (!isGroupIdAvailable(job)) {
+ Job blockingJob = activeGroupIds.get(job.getGroupId());
+ blockingJob.resetRunStats();
+ }
+ }
+
synchronized void push(Job job) {
jobQueue.addFirst(job);
}
@@ -73,9 +90,9 @@ class JobQueue {
while (iterator.hasNext()) {
Job job = iterator.next();
- if (job.isRequirementsMet() && isGroupIdAvailable(job.getGroupId())) {
+ if (job.isRequirementsMet() && isGroupIdAvailable(job)) {
iterator.remove();
- setGroupIdUnavailable(job.getGroupId());
+ setGroupIdUnavailable(job);
return job;
}
}
@@ -83,13 +100,19 @@ class JobQueue {
return null;
}
- private boolean isGroupIdAvailable(String groupId) {
- return groupId == null || !activeGroupIds.contains(groupId);
+ private boolean isJobActive(@NonNull Job job) {
+ return job.getRetryUntil() > 0 && job.getRunIteration() > 0;
}
- private void setGroupIdUnavailable(String groupId) {
+ private boolean isGroupIdAvailable(@NonNull Job requester) {
+ String groupId = requester.getGroupId();
+ return groupId == null || !activeGroupIds.containsKey(groupId) || activeGroupIds.get(groupId).equals(requester);
+ }
+
+ private void setGroupIdUnavailable(@NonNull Job job) {
+ String groupId = job.getGroupId();
if (groupId != null) {
- activeGroupIds.add(groupId);
+ activeGroupIds.put(groupId, job);
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/BackoffReceiver.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/BackoffReceiver.java
new file mode 100644
index 0000000000..35a10cd9d5
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/jobmanager/requirements/BackoffReceiver.java
@@ -0,0 +1,35 @@
+package org.thoughtcrime.securesms.jobmanager.requirements;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.BuildConfig;
+
+import java.util.UUID;
+
+public class BackoffReceiver extends BroadcastReceiver {
+
+ private static final String TAG = BackoffReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "Received an alarm to retry a job with ID: " + intent.getAction());
+ ApplicationContext.getInstance(context).getJobManager().onRequirementStatusChanged();
+ }
+
+ public static void setUniqueAlarm(@NonNull Context context, long time) {
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(context, BackoffReceiver.class);
+
+ intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString());
+ alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, 0));
+
+ Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms with ID: " + intent.getAction());
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java
new file mode 100644
index 0000000000..f0ef170f93
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java
@@ -0,0 +1,50 @@
+package org.thoughtcrime.securesms.jobmanager.requirements;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.thoughtcrime.securesms.jobmanager.Job;
+import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Uses exponential backoff to re-schedule network jobs to be retried in the future.
+ */
+public class NetworkBackoffRequirement implements Requirement, ContextDependent {
+
+ private static final long MAX_WAIT = TimeUnit.SECONDS.toMillis(30);
+
+ private transient Context context;
+
+ public NetworkBackoffRequirement(@NonNull Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ @Override
+ public boolean isPresent(@NonNull Job job) {
+ return new NetworkRequirement(context).isPresent() && System.currentTimeMillis() >= calculateNextRunTime(job);
+ }
+
+ @Override
+ public void onRetry(@NonNull Job job) {
+ if (!(new NetworkRequirement(context).isPresent())) {
+ job.resetRunStats();
+ return;
+ }
+
+ BackoffReceiver.setUniqueAlarm(context, NetworkBackoffRequirement.calculateNextRunTime(job));
+ }
+
+ @Override
+ public void setContext(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ private static long calculateNextRunTime(@NonNull Job job) {
+ long targetTime = job.getLastRunTime() + (long) (Math.pow(2, job.getRunIteration() - 1) * 1000);
+ long furthestTime = System.currentTimeMillis() + MAX_WAIT;
+
+ return Math.min(targetTime, Math.min(furthestTime, job.getRetryUntil()));
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java
index f20ff54820..050fac6a27 100644
--- a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java
+++ b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java
@@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
/**
* A requirement that is satisfied when a network connection is present.
*/
-public class NetworkRequirement implements Requirement, ContextDependent {
+public class NetworkRequirement extends SimpleRequirement implements ContextDependent {
private transient Context context;
diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java
index a294778a5a..3631efb36c 100644
--- a/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java
+++ b/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java
@@ -16,6 +16,10 @@
*/
package org.thoughtcrime.securesms.jobmanager.requirements;
+import android.support.annotation.NonNull;
+
+import org.thoughtcrime.securesms.jobmanager.Job;
+
import java.io.Serializable;
/**
@@ -25,5 +29,7 @@ public interface Requirement extends Serializable {
/**
* @return true if the requirement is satisfied, false otherwise.
*/
- public boolean isPresent();
+ boolean isPresent(@NonNull Job job);
+
+ void onRetry(@NonNull Job job);
}
diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java
new file mode 100644
index 0000000000..e6e852b00d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java
@@ -0,0 +1,19 @@
+package org.thoughtcrime.securesms.jobmanager.requirements;
+
+import android.support.annotation.NonNull;
+
+import org.thoughtcrime.securesms.jobmanager.Job;
+
+public abstract class SimpleRequirement implements Requirement {
+
+ @Override
+ public boolean isPresent(@NonNull Job job) {
+ return isPresent();
+ }
+
+ @Override
+ public void onRetry(@NonNull Job job) {
+ }
+
+ public abstract boolean isPresent();
+}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java
index cc8b3dd528..b7419ac93a 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java
@@ -12,7 +12,8 @@ import java.io.IOException;
public class PushContentReceiveJob extends PushReceivedJob {
- private static final String TAG = PushContentReceiveJob.class.getSimpleName();
+ private static final long serialVersionUID = 5685475456901715638L;
+ private static final String TAG = PushContentReceiveJob.class.getSimpleName();
private final String data;
diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
index 87256a37dc..9d3bb631ca 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
-import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
+import org.thoughtcrime.securesms.jobmanager.requirements.NetworkBackoffRequirement;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@@ -38,10 +38,12 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
public abstract class PushSendJob extends SendJob {
- private static final String TAG = PushSendJob.class.getSimpleName();
+ private static final long serialVersionUID = 5906098204770900739L;
+ private static final String TAG = PushSendJob.class.getSimpleName();
protected PushSendJob(Context context, JobParameters parameters) {
super(context, parameters);
@@ -52,8 +54,8 @@ public abstract class PushSendJob extends SendJob {
builder.withPersistence();
builder.withGroupId(destination.serialize());
builder.withRequirement(new MasterSecretRequirement(context));
- builder.withRequirement(new NetworkRequirement(context));
- builder.withRetryCount(5);
+ builder.withRequirement(new NetworkBackoffRequirement(context));
+ builder.withRetryDuration(TimeUnit.DAYS.toMillis(1));
return builder.create();
}
diff --git a/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java b/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java
index 86eb182d6b..a2ba460b92 100644
--- a/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java
@@ -29,7 +29,8 @@ import java.util.ArrayList;
public class SmsSendJob extends SendJob {
- private static final String TAG = SmsSendJob.class.getSimpleName();
+ private static final long serialVersionUID = -5118520036244759718L;
+ private static final String TAG = SmsSendJob.class.getSimpleName();
private final long messageId;
diff --git a/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java b/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java
index f2d2b3be9c..1d26dcdc43 100644
--- a/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java
@@ -18,7 +18,8 @@ import org.thoughtcrime.securesms.service.SmsDeliveryListener;
public class SmsSentJob extends MasterSecretJob {
- private static final String TAG = SmsSentJob.class.getSimpleName();
+ private static final long serialVersionUID = -2624694558755317560L;
+ private static final String TAG = SmsSentJob.class.getSimpleName();
private final long messageId;
private final String action;
diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java
index 00888ee808..b33f424614 100644
--- a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java
+++ b/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java
@@ -4,9 +4,10 @@ import android.content.Context;
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
+import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement;
import org.thoughtcrime.securesms.service.KeyCachingService;
-public class MasterSecretRequirement implements Requirement, ContextDependent {
+public class MasterSecretRequirement extends SimpleRequirement implements ContextDependent {
private transient Context context;
diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java
index 245c1ee360..5dcf686b51 100644
--- a/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java
+++ b/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java
@@ -5,8 +5,9 @@ import android.content.Context;
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
+import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement;
-public class NetworkOrServiceRequirement implements Requirement, ContextDependent {
+public class NetworkOrServiceRequirement extends SimpleRequirement implements ContextDependent {
private transient Context context;
diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java
index 39e081dc86..49a0368c55 100644
--- a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java
+++ b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java
@@ -4,9 +4,10 @@ import android.content.Context;
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
+import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement;
import org.thoughtcrime.securesms.sms.TelephonyServiceState;
-public class ServiceRequirement implements Requirement, ContextDependent {
+public class ServiceRequirement extends SimpleRequirement implements ContextDependent {
private static final String TAG = ServiceRequirement.class.getSimpleName();
diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java
index b4d6e339fb..76dd67b663 100644
--- a/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java
+++ b/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java
@@ -6,9 +6,10 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
+import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-public class SqlCipherMigrationRequirement implements Requirement, ContextDependent {
+public class SqlCipherMigrationRequirement extends SimpleRequirement implements ContextDependent {
@SuppressWarnings("unused")
private static final String TAG = SqlCipherMigrationRequirement.class.getSimpleName();