Skip to content

Commit

Permalink
Implement delivery receipts.
Browse files Browse the repository at this point in the history
1) Support a "receipt" push message type.

2) Identify messages by timestamp.

3) Introduce a JobManager to handle the queue for network
   dependent jobs.
  • Loading branch information
moxie0 committed Oct 21, 2014
1 parent 8d6b9ae commit 36ec1d8
Show file tree
Hide file tree
Showing 48 changed files with 730 additions and 262 deletions.
2 changes: 1 addition & 1 deletion AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
android:protectionLevel="signature" />
<uses-permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE" />

<application android:name="org.thoughtcrime.securesms.ApplicationListener"
<application android:name=".ApplicationContext"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@style/TextSecure.LightTheme">
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ 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'

Expand All @@ -56,6 +57,8 @@ dependencyVerification {
'com.madgag:sc-light-jdk15on:931f39d351429fb96c2f749e7ecb1a256a8ebbf5edca7995c9cc085b94d1841d',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.path:android-priority-jobqueue:af8d0dc930c3640518e9548ec887cf7871ab5e3c1ea634a4553dd5c30dd6e4a8'
]
}

Expand Down
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repositories {
}

dependencies {
compile 'com.google.protobuf:protobuf-java:2.4.1'
compile 'com.google.protobuf:protobuf-java:2.5.0'
compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
compile 'org.whispersystems:gson:2.2.4'
Expand Down
1 change: 1 addition & 0 deletions library/protobuf/IncomingPushMessageSignal.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ message IncomingPushMessageSignal {
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,12 @@ public boolean isSecureMessage() {
public boolean isPreKeyBundle() {
return getType() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
}

public boolean isReceipt() {
return getType() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
}

public boolean isPlaintext() {
return getType() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.whispersystems.textsecure.push;

import java.util.LinkedList;
import java.util.List;

public class OutgoingPushMessageList {
Expand All @@ -9,9 +8,14 @@ public class OutgoingPushMessageList {

private String relay;

private long timestamp;

private List<OutgoingPushMessage> messages;

public OutgoingPushMessageList(String destination, String relay, List<OutgoingPushMessage> messages) {
public OutgoingPushMessageList(String destination, long timestamp, String relay,
List<OutgoingPushMessage> messages)
{
this.timestamp = timestamp;
this.destination = destination;
this.relay = relay;
this.messages = messages;
Expand All @@ -28,4 +32,8 @@ public List<OutgoingPushMessage> getMessages() {
public String getRelay() {
return relay;
}

public long getTimestamp() {
return timestamp;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 72 additions & 44 deletions library/src/org/whispersystems/textsecure/push/PushServiceSocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.textsecure.push.exceptions.AuthorizationFailedException;
import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException;
import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.push.exceptions.RateLimitException;
import org.whispersystems.textsecure.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.BlacklistingTrustManager;
import org.whispersystems.textsecure.util.Util;
Expand Down Expand Up @@ -75,6 +83,7 @@ public class PushServiceSocket {
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
private static final String MESSAGE_PATH = "/v1/messages/%s";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";

private static final boolean ENFORCE_SSL = true;
Expand Down Expand Up @@ -109,6 +118,16 @@ public void verifyAccount(String verificationCode, String signalingKey,
"PUT", new Gson().toJson(signalingKeyEntity));
}

public void sendReceipt(String destination, long messageId, String relay) throws IOException {
String path = String.format(RECEIPT_PATH, destination, messageId);

if (!Util.isEmpty(relay)) {
path += "?relay=" + relay;
}

makeRequest(path, "PUT", null);
}

public void registerGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
Expand Down Expand Up @@ -380,68 +399,77 @@ private void uploadExternalFile(String method, String url, byte[] data)
}

private String makeRequest(String urlFragment, String method, String body)
throws IOException
throws NonSuccessfulResponseCodeException, PushNetworkException
{
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
String response = Util.readFully(connection.getInputStream());

connection.disconnect();
try {
String response = Util.readFully(connection.getInputStream());
connection.disconnect();

return response;
return response;
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
}

private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
throws IOException
throws NonSuccessfulResponseCodeException, PushNetworkException
{
HttpURLConnection connection = getConnection(urlFragment, method);
try {
HttpURLConnection connection = getConnection(urlFragment, method);

if (body != null) {
connection.setDoOutput(true);
}
if (body != null) {
connection.setDoOutput(true);
}

connection.connect();
connection.connect();

if (body != null) {
Log.w("PushServiceSocket", method + " -- " + body);
OutputStream out = connection.getOutputStream();
out.write(body.getBytes());
out.close();
}
if (body != null) {
Log.w("PushServiceSocket", method + " -- " + body);
OutputStream out = connection.getOutputStream();
out.write(body.getBytes());
out.close();
}

if (connection.getResponseCode() == 413) {
connection.disconnect();
throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode());
}
if (connection.getResponseCode() == 413) {
connection.disconnect();
throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode());
}

if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) {
connection.disconnect();
throw new AuthorizationFailedException("Authorization failed!");
}
if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) {
connection.disconnect();
throw new AuthorizationFailedException("Authorization failed!");
}

if (connection.getResponseCode() == 404) {
connection.disconnect();
throw new NotFoundException("Not found");
}
if (connection.getResponseCode() == 404) {
connection.disconnect();
throw new NotFoundException("Not found");
}

if (connection.getResponseCode() == 409) {
String response = Util.readFully(connection.getErrorStream());
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
}
if (connection.getResponseCode() == 409) {
String response = Util.readFully(connection.getErrorStream());
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
}

if (connection.getResponseCode() == 410) {
String response = Util.readFully(connection.getErrorStream());
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
}
if (connection.getResponseCode() == 410) {
String response = Util.readFully(connection.getErrorStream());
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
}

if (connection.getResponseCode() == 417) {
throw new ExpectationFailedException();
}
if (connection.getResponseCode() == 417) {
throw new ExpectationFailedException();
}

if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode() +
" " + connection.getResponseMessage());
}

return connection;
return connection;
} catch (IOException e) {
throw new PushNetworkException(e);
}
}

private HttpURLConnection getConnection(String urlFragment, String method) throws IOException {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.whispersystems.textsecure.push.exceptions;

public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
public AuthorizationFailedException(String s) {
super(s);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.whispersystems.textsecure.push.exceptions;

public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
}

0 comments on commit 36ec1d8

Please sign in to comment.