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 boolean isGroupIdAvailable(@NonNull Job requester) { + String groupId = requester.getGroupId(); + return groupId == null || !activeGroupIds.containsKey(groupId) || activeGroupIds.get(groupId).equals(requester); } - private void setGroupIdUnavailable(String groupId) { + 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();