From b308996885c14e36a788e7d0ddcf98d793387206 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 4 Aug 2014 16:15:13 -0700 Subject: [PATCH] Switch to using our own JobManager. // FREEBIE --- build.gradle | 8 +- .../textsecure/push/PushServiceSocket.java | 106 ++++++++++-------- .../securesms/ApplicationContext.java | 20 ++-- .../securesms/gcm/GcmBroadcastReceiver.java | 7 +- .../securesms/jobs/ContextInjector.java | 22 ---- .../securesms/jobs/ContextJob.java | 14 ++- .../securesms/jobs/DeliveryReceiptJob.java | 24 ++-- .../jobs/EncryptingJobSerializer.java | 57 ++++++++++ .../securesms/jobs/GcmRefreshJob.java | 15 +-- .../securesms/jobs/JobLogger.java | 27 ----- .../securesms/jobs/Priorities.java | 8 -- .../securesms/util/ParcelUtil.java | 28 +++++ 12 files changed, 184 insertions(+), 152 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/jobs/ContextInjector.java create mode 100644 src/org/thoughtcrime/securesms/jobs/EncryptingJobSerializer.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/JobLogger.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/Priorities.java create mode 100644 src/org/thoughtcrime/securesms/util/ParcelUtil.java diff --git a/build.gradle b/build.gradle index 483d14b627..846f9b9752 100644 --- a/build.gradle +++ b/build.gradle @@ -36,14 +36,13 @@ dependencies { compile 'com.astuetz:pagerslidingtabstrip:1.0.1' compile 'org.w3c:smil:1.0.0' compile 'org.apache.httpcomponents:httpclient-android:4.3.5' - compile 'com.path:android-priority-jobqueue:1.1.2' androidTestCompile 'com.squareup:fest-android:1.0.8' - - compile project(':library') - androidTestCompile 'com.google.dexmaker:dexmaker:1.1' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1' + + compile project(':library') + compile project(':jobqueue') } dependencyVerification { @@ -58,7 +57,6 @@ dependencyVerification { 'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab', 'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51', 'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74', - 'com.path:android-priority-jobqueue:af8d0dc930c3640518e9548ec887cf7871ab5e3c1ea634a4553dd5c30dd6e4a8' ] } diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index c2d0a11bcc..6af77c1603 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -416,63 +416,58 @@ public class PushServiceSocket { private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body) throws NonSuccessfulResponseCodeException, PushNetworkException { + HttpURLConnection connection = getConnection(urlFragment, method, body); + int responseCode; + String responseMessage; + String response; + try { - HttpURLConnection connection = getConnection(urlFragment, method); + responseCode = connection.getResponseCode(); + responseMessage = connection.getResponseMessage(); + } catch (IOException ioe) { + throw new PushNetworkException(ioe); + } - if (body != null) { - connection.setDoOutput(true); - } - - connection.connect(); - - if (body != null) { - Log.w("PushServiceSocket", method + " -- " + body); - OutputStream out = connection.getOutputStream(); - out.write(body.getBytes()); - out.close(); - } - - if (connection.getResponseCode() == 413) { + switch (responseCode) { + case 413: connection.disconnect(); - throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode()); - } - - if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) { + throw new RateLimitException("Rate limit exceeded: " + responseCode); + case 401: + case 403: connection.disconnect(); throw new AuthorizationFailedException("Authorization failed!"); - } - - if (connection.getResponseCode() == 404) { + case 404: connection.disconnect(); throw new NotFoundException("Not found"); - } - - if (connection.getResponseCode() == 409) { - String response = Util.readFully(connection.getErrorStream()); + case 409: + try { + response = Util.readFully(connection.getErrorStream()); + } catch (IOException e) { + throw new PushNetworkException(e); + } throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class)); - } - - if (connection.getResponseCode() == 410) { - String response = Util.readFully(connection.getErrorStream()); + case 410: + try { + response = Util.readFully(connection.getErrorStream()); + } catch (IOException e) { + throw new PushNetworkException(e); + } throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class)); - } - - if (connection.getResponseCode() == 417) { + case 417: throw new ExpectationFailedException(); - } - - if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) { - throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode() + - " " + connection.getResponseMessage()); - } - - return connection; - } catch (IOException e) { - throw new PushNetworkException(e); } + + if (responseCode != 200 && responseCode != 204) { + throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + + responseMessage); + } + + return connection; } - private HttpURLConnection getConnection(String urlFragment, String method) throws IOException { + private HttpURLConnection getConnection(String urlFragment, String method, String body) + throws PushNetworkException + { try { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, trustManagers, null); @@ -481,11 +476,11 @@ public class PushServiceSocket { Log.w("PushServiceSocket", "Push service URL: " + serviceUrl); Log.w("PushServiceSocket", "Opening URL: " + url); - HttpURLConnection connection = (HttpURLConnection)url.openConnection(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); if (ENFORCE_SSL) { - ((HttpsURLConnection)connection).setSSLSocketFactory(context.getSocketFactory()); - ((HttpsURLConnection)connection).setHostnameVerifier(new StrictHostnameVerifier()); + ((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory()); + ((HttpsURLConnection) connection).setHostnameVerifier(new StrictHostnameVerifier()); } connection.setRequestMethod(method); @@ -495,13 +490,26 @@ public class PushServiceSocket { connection.setRequestProperty("Authorization", getAuthorizationHeader()); } + if (body != null) { + connection.setDoOutput(true); + } + + connection.connect(); + + if (body != null) { + Log.w("PushServiceSocket", method + " -- " + body); + OutputStream out = connection.getOutputStream(); + out.write(body.getBytes()); + out.close(); + } + return connection; + } catch (IOException e) { + throw new PushNetworkException(e); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } catch (KeyManagementException e) { throw new AssertionError(e); - } catch (MalformedURLException e) { - throw new AssertionError(e); } } diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 06e1635508..2df9848caa 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -19,14 +19,12 @@ package org.thoughtcrime.securesms; import android.app.Application; import android.content.Context; -import com.path.android.jobqueue.JobManager; -import com.path.android.jobqueue.config.Configuration; - import org.thoughtcrime.securesms.crypto.PRNGFixes; -import org.thoughtcrime.securesms.jobs.ContextInjector; +import org.thoughtcrime.securesms.jobs.EncryptingJobSerializer; import org.thoughtcrime.securesms.jobs.GcmRefreshJob; -import org.thoughtcrime.securesms.jobs.JobLogger; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.jobqueue.JobManager; +import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider; /** * Will be called once when the TextSecure process is created. @@ -60,20 +58,16 @@ public class ApplicationContext extends Application { } private void initializeJobManager() { - Configuration configuration = new Configuration.Builder(this) - .minConsumerCount(1) - .injector(new ContextInjector(this)) - .customLogger(new JobLogger()) - .build(); - - this.jobManager = new JobManager(this, configuration); + this.jobManager = new JobManager(this, "TextSecureJobs", + new NetworkRequirementProvider(this), + new EncryptingJobSerializer(this), 5); } private void initializeGcmCheck() { if (TextSecurePreferences.isPushRegistered(this) && TextSecurePreferences.getGcmRegistrationId(this) == null) { - this.jobManager.addJob(new GcmRefreshJob()); + this.jobManager.add(new GcmRefreshJob(this)); } } diff --git a/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java b/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java index 4086e8adc4..d6028127c6 100644 --- a/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java +++ b/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java @@ -6,12 +6,12 @@ import android.content.Intent; import android.util.Log; import com.google.android.gms.gcm.GoogleCloudMessaging; -import com.path.android.jobqueue.JobManager; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.jobs.DeliveryReceiptJob; import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.jobqueue.JobManager; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.NotInDirectoryException; @@ -68,8 +68,9 @@ public class GcmBroadcastReceiver extends BroadcastReceiver { if (!message.isReceipt()) { JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - jobManager.addJob(new DeliveryReceiptJob(message.getSource(), message.getTimestampMillis(), - message.getRelay())); + jobManager.add(new DeliveryReceiptJob(context, message.getSource(), + message.getTimestampMillis(), + message.getRelay())); } } catch (IOException e) { Log.w(TAG, e); diff --git a/src/org/thoughtcrime/securesms/jobs/ContextInjector.java b/src/org/thoughtcrime/securesms/jobs/ContextInjector.java deleted file mode 100644 index 3a5cb82217..0000000000 --- a/src/org/thoughtcrime/securesms/jobs/ContextInjector.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; - -import com.path.android.jobqueue.BaseJob; -import com.path.android.jobqueue.di.DependencyInjector; - -public class ContextInjector implements DependencyInjector { - - private final Context context; - - public ContextInjector(Context context) { - this.context = context; - } - - @Override - public void inject(BaseJob job) { - if (job instanceof ContextJob) { - ((ContextJob)job).setContext(context); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/ContextJob.java b/src/org/thoughtcrime/securesms/jobs/ContextJob.java index cebcd21546..e68f1a0a21 100644 --- a/src/org/thoughtcrime/securesms/jobs/ContextJob.java +++ b/src/org/thoughtcrime/securesms/jobs/ContextJob.java @@ -2,15 +2,17 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; -import com.path.android.jobqueue.Job; -import com.path.android.jobqueue.Params; +import org.whispersystems.jobqueue.Job; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.dependencies.ContextDependent; -public abstract class ContextJob extends Job { +public abstract class ContextJob extends Job implements ContextDependent { - transient protected Context context; + protected transient Context context; - protected ContextJob(Params params) { - super(params); + protected ContextJob(Context context, JobParameters parameters) { + super(parameters); + this.context = context; } public void setContext(Context context) { diff --git a/src/org/thoughtcrime/securesms/jobs/DeliveryReceiptJob.java b/src/org/thoughtcrime/securesms/jobs/DeliveryReceiptJob.java index 5c2d2e92e5..edb87ca879 100644 --- a/src/org/thoughtcrime/securesms/jobs/DeliveryReceiptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/DeliveryReceiptJob.java @@ -1,10 +1,11 @@ package org.thoughtcrime.securesms.jobs; +import android.content.Context; import android.util.Log; -import com.path.android.jobqueue.Params; - import org.thoughtcrime.securesms.push.PushServiceSocketFactory; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.textsecure.push.exceptions.PushNetworkException; @@ -17,8 +18,12 @@ public class DeliveryReceiptJob extends ContextJob { private final long timestamp; private final String relay; - public DeliveryReceiptJob(String destination, long timestamp, String relay) { - super(new Params(Priorities.HIGH).requireNetwork().persist()); + public DeliveryReceiptJob(Context context, String destination, long timestamp, String relay) { + super(context, JobParameters.newBuilder() + .withRequirement(new NetworkRequirement(context)) + .withPersistence() + .withRetryCount(50) + .create()); this.destination = destination; this.timestamp = timestamp; @@ -36,21 +41,16 @@ public class DeliveryReceiptJob extends ContextJob { } @Override - protected void onCancel() { + public void onCanceled() { Log.w(TAG, "Failed to send receipt after retry exhausted!"); } @Override - protected boolean shouldReRunOnThrowable(Throwable throwable) { + public boolean onShouldRetry(Throwable throwable) { Log.w(TAG, throwable); if (throwable instanceof NonSuccessfulResponseCodeException) return false; - if (throwable instanceof PushNetworkException) return true; + if (throwable instanceof PushNetworkException) return true; return false; } - - @Override - protected int getRetryLimit() { - return 50; - } } diff --git a/src/org/thoughtcrime/securesms/jobs/EncryptingJobSerializer.java b/src/org/thoughtcrime/securesms/jobs/EncryptingJobSerializer.java new file mode 100644 index 0000000000..8b96cf6f20 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/EncryptingJobSerializer.java @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.jobs; + +import android.content.Context; + +import org.thoughtcrime.securesms.util.ParcelUtil; +import org.whispersystems.jobqueue.EncryptionKeys; +import org.whispersystems.jobqueue.Job; +import org.whispersystems.jobqueue.persistence.JavaJobSerializer; +import org.whispersystems.jobqueue.persistence.JobSerializer; +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.textsecure.crypto.MasterCipher; +import org.whispersystems.textsecure.crypto.MasterSecret; + +import java.io.IOException; + +public class EncryptingJobSerializer implements JobSerializer { + + private final JavaJobSerializer delegate; + + public EncryptingJobSerializer(Context context) { + this.delegate = new JavaJobSerializer(context); + } + + @Override + public String serialize(Job job) throws IOException { + String plaintext = delegate.serialize(job); + + if (job.getEncryptionKeys() != null) { + MasterSecret masterSecret = ParcelUtil.deserialize(job.getEncryptionKeys().getEncoded(), + MasterSecret.CREATOR); + MasterCipher masterCipher = new MasterCipher(masterSecret); + + return masterCipher.encryptBody(plaintext); + } else { + return plaintext; + } + } + + @Override + public Job deserialize(EncryptionKeys keys, boolean encrypted, String serialized) throws IOException { + try { + String plaintext; + + if (encrypted) { + MasterSecret masterSecret = ParcelUtil.deserialize(keys.getEncoded(), MasterSecret.CREATOR); + MasterCipher masterCipher = new MasterCipher(masterSecret); + plaintext = masterCipher.decryptBody(serialized); + } else { + plaintext = serialized; + } + + return delegate.deserialize(keys, encrypted, plaintext); + } catch (InvalidMessageException e) { + throw new IOException(e); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/GcmRefreshJob.java b/src/org/thoughtcrime/securesms/jobs/GcmRefreshJob.java index 97369f9dbb..07ed043217 100644 --- a/src/org/thoughtcrime/securesms/jobs/GcmRefreshJob.java +++ b/src/org/thoughtcrime/securesms/jobs/GcmRefreshJob.java @@ -1,19 +1,19 @@ package org.thoughtcrime.securesms.jobs; +import android.content.Context; import android.util.Log; import android.widget.Toast; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.gcm.GoogleCloudMessaging; -import com.path.android.jobqueue.Params; import org.thoughtcrime.securesms.push.PushServiceSocketFactory; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.textsecure.push.PushServiceSocket; -import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException; import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException; -import org.whispersystems.textsecure.push.exceptions.PushNetworkException; public class GcmRefreshJob extends ContextJob { @@ -21,8 +21,8 @@ public class GcmRefreshJob extends ContextJob { public static final String REGISTRATION_ID = "312334754206"; - public GcmRefreshJob() { - super(new Params(Priorities.NORMAL).requireNetwork()); + public GcmRefreshJob(Context context) { + super(context, JobParameters.newBuilder().withRequirement(new NetworkRequirement(context)).create()); } @Override @@ -49,13 +49,14 @@ public class GcmRefreshJob extends ContextJob { } @Override - protected void onCancel() { + public void onCanceled() { Log.w(TAG, "GCM reregistration failed after retry attempt exhaustion!"); } @Override - protected boolean shouldReRunOnThrowable(Throwable throwable) { + public boolean onShouldRetry(Throwable throwable) { if (throwable instanceof NonSuccessfulResponseCodeException) return false; return true; } + } diff --git a/src/org/thoughtcrime/securesms/jobs/JobLogger.java b/src/org/thoughtcrime/securesms/jobs/JobLogger.java deleted file mode 100644 index f78fe2e921..0000000000 --- a/src/org/thoughtcrime/securesms/jobs/JobLogger.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.util.Log; - -import com.path.android.jobqueue.log.CustomLogger; - -public class JobLogger implements CustomLogger { - @Override - public boolean isDebugEnabled() { - return false; - } - - @Override - public void d(String text, Object... args) { - Log.w("JobManager", String.format(text, args)); - } - - @Override - public void e(Throwable t, String text, Object... args) { - Log.w("JobManager", String.format(text, args), t); - } - - @Override - public void e(String text, Object... args) { - Log.w("JobManager", String.format(text, args)); - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/Priorities.java b/src/org/thoughtcrime/securesms/jobs/Priorities.java deleted file mode 100644 index 347fb2b736..0000000000 --- a/src/org/thoughtcrime/securesms/jobs/Priorities.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -public class Priorities { - - public static final int NORMAL = 500; - public static final int HIGH = 1000; - -} diff --git a/src/org/thoughtcrime/securesms/util/ParcelUtil.java b/src/org/thoughtcrime/securesms/util/ParcelUtil.java new file mode 100644 index 0000000000..c1eda1232b --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/ParcelUtil.java @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.util; + +import android.os.Parcel; +import android.os.Parcelable; + +public class ParcelUtil { + + public static byte[] serialize(Parcelable parceable) { + Parcel parcel = Parcel.obtain(); + parceable.writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + return bytes; + } + + public static Parcel deserialize(byte[] bytes) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + return parcel; + } + + public static T deserialize(byte[] bytes, Parcelable.Creator creator) { + Parcel parcel = deserialize(bytes); + return creator.createFromParcel(parcel); + } + +}