Implement delivery receipts.

1) Support a "receipt" push message type.

2) Identify messages by timestamp.

3) Introduce a JobManager to handle the queue for network
   dependent jobs.
This commit is contained in:
Moxie Marlinspike 2014-07-25 15:14:29 -07:00
parent 8d6b9ae43e
commit 36ec1d84a1
48 changed files with 739 additions and 271 deletions

View File

@ -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">

View File

@ -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'
@ -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'
]
}

View File

@ -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'

View File

@ -10,6 +10,7 @@ message IncomingPushMessageSignal {
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;

View File

@ -1,9 +0,0 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class AuthorizationFailedException extends IOException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@ -1,6 +0,0 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class ExpectationFailedException extends IOException {
}

View File

@ -138,4 +138,12 @@ public class IncomingPushMessage implements Parcelable {
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;
}
}

View File

@ -1,9 +0,0 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class NotFoundException extends IOException {
public NotFoundException(String s) {
super(s);
}
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecure.push;
import java.util.LinkedList;
import java.util.List;
public class OutgoingPushMessageList {
@ -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;
@ -28,4 +32,8 @@ public class OutgoingPushMessageList {
public String getRelay() {
return relay;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -240,6 +240,10 @@ public final class PushMessageProtos {
* <code>PLAINTEXT = 4;</code>
*/
PLAINTEXT(4, 4),
/**
* <code>RECEIPT = 5;</code>
*/
RECEIPT(5, 5),
;
/**
@ -262,6 +266,10 @@ public final class PushMessageProtos {
* <code>PLAINTEXT = 4;</code>
*/
public static final int PLAINTEXT_VALUE = 4;
/**
* <code>RECEIPT = 5;</code>
*/
public static final int RECEIPT_VALUE = 5;
public final int getNumber() { return value; }
@ -273,6 +281,7 @@ public final class PushMessageProtos {
case 2: return KEY_EXCHANGE;
case 3: return PREKEY_BUNDLE;
case 4: return PLAINTEXT;
case 5: return RECEIPT;
default: return null;
}
}
@ -4079,28 +4088,28 @@ public final class PushMessageProtos {
static {
java.lang.String[] descriptorData = {
"\n\037IncomingPushMessageSignal.proto\022\ntexts" +
"ecure\"\207\002\n\031IncomingPushMessageSignal\0228\n\004t" +
"ecure\"\224\002\n\031IncomingPushMessageSignal\0228\n\004t" +
"ype\030\001 \001(\0162*.textsecure.IncomingPushMessa" +
"geSignal.Type\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceD" +
"evice\030\007 \001(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030" +
"\005 \001(\004\022\017\n\007message\030\006 \001(\014\"W\n\004Type\022\013\n\007UNKNOW" +
"\005 \001(\004\022\017\n\007message\030\006 \001(\014\"d\n\004Type\022\013\n\007UNKNOW" +
"N\020\000\022\016\n\nCIPHERTEXT\020\001\022\020\n\014KEY_EXCHANGE\020\002\022\021\n" +
"\rPREKEY_BUNDLE\020\003\022\r\n\tPLAINTEXT\020\004\"\207\004\n\022Push" +
"MessageContent\022\014\n\004body\030\001 \001(\t\022E\n\013attachme" +
"nts\030\002 \003(\01320.textsecure.PushMessageConten",
"t.AttachmentPointer\022:\n\005group\030\003 \001(\0132+.tex" +
"tsecure.PushMessageContent.GroupContext\022" +
"\r\n\005flags\030\004 \001(\r\032A\n\021AttachmentPointer\022\n\n\002i" +
"d\030\001 \001(\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(" +
"\014\032\363\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022>\n\004type\030\002" +
" \001(\01620.textsecure.PushMessageContent.Gro" +
"upContext.Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030" +
"\004 \003(\t\022@\n\006avatar\030\005 \001(\01320.textsecure.PushM" +
"essageContent.AttachmentPointer\"6\n\004Type\022" +
"\013\n\007UNKNOWN\020\000\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n",
"\004QUIT\020\003\"\030\n\005Flags\022\017\n\013END_SESSION\020\001B7\n\"org" +
".whispersystems.textsecure.pushB\021PushMes" +
"sageProtos"
"\rPREKEY_BUNDLE\020\003\022\r\n\tPLAINTEXT\020\004\022\013\n\007RECEI" +
"PT\020\005\"\207\004\n\022PushMessageContent\022\014\n\004body\030\001 \001(" +
"\t\022E\n\013attachments\030\002 \003(\01320.textsecure.Push",
"MessageContent.AttachmentPointer\022:\n\005grou" +
"p\030\003 \001(\0132+.textsecure.PushMessageContent." +
"GroupContext\022\r\n\005flags\030\004 \001(\r\032A\n\021Attachmen" +
"tPointer\022\n\n\002id\030\001 \001(\006\022\023\n\013contentType\030\002 \001(" +
"\t\022\013\n\003key\030\003 \001(\014\032\363\001\n\014GroupContext\022\n\n\002id\030\001 " +
"\001(\014\022>\n\004type\030\002 \001(\01620.textsecure.PushMessa" +
"geContent.GroupContext.Type\022\014\n\004name\030\003 \001(" +
"\t\022\017\n\007members\030\004 \003(\t\022@\n\006avatar\030\005 \001(\01320.tex" +
"tsecure.PushMessageContent.AttachmentPoi" +
"nter\"6\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006UPDATE\020\001\022\013\n",
"\007DELIVER\020\002\022\010\n\004QUIT\020\003\"\030\n\005Flags\022\017\n\013END_SES" +
"SION\020\001B7\n\"org.whispersystems.textsecure." +
"pushB\021PushMessageProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {

View File

@ -25,9 +25,17 @@ import com.google.thoughtcrimegson.JsonParseException;
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;
@ -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;
@ -109,6 +118,16 @@ public class PushServiceSocket {
"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));
@ -380,68 +399,77 @@ public class PushServiceSocket {
}
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();
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() == 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() == 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() == 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);
}
connection.connect();
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() == 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() == 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() == 417) {
throw new ExpectationFailedException();
}
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
return connection;
}
private HttpURLConnection getConnection(String urlFragment, String method) throws IOException {

View File

@ -1,10 +0,0 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class RateLimitException extends IOException {
public RateLimitException(String s) {
super(s);
}
}

View File

@ -1,16 +0,0 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class StaleDevicesException extends IOException {
private final StaleDevices staleDevices;
public StaleDevicesException(StaleDevices staleDevices) {
this.staleDevices = staleDevices;
}
public StaleDevices getStaleDevices() {
return staleDevices;
}
}

View File

@ -0,0 +1,7 @@
package org.whispersystems.textsecure.push.exceptions;
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@ -0,0 +1,4 @@
package org.whispersystems.textsecure.push.exceptions;
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
}

View File

@ -1,8 +1,8 @@
package org.whispersystems.textsecure.push;
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
import org.whispersystems.textsecure.push.MismatchedDevices;
public class MismatchedDevicesException extends IOException {
public class MismatchedDevicesException extends NonSuccessfulResponseCodeException {
private final MismatchedDevices mismatchedDevices;

View File

@ -0,0 +1,14 @@
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
public class NonSuccessfulResponseCodeException extends IOException {
public NonSuccessfulResponseCodeException() {
super();
}
public NonSuccessfulResponseCodeException(String s) {
super(s);
}
}

View File

@ -0,0 +1,9 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class NotFoundException extends NonSuccessfulResponseCodeException {
public NotFoundException(String s) {
super(s);
}
}

View File

@ -0,0 +1,9 @@
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
public class PushNetworkException extends IOException {
public PushNetworkException(Exception exception) {
super(exception);
}
}

View File

@ -0,0 +1,10 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class RateLimitException extends NonSuccessfulResponseCodeException {
public RateLimitException(String s) {
super(s);
}
}

View File

@ -0,0 +1,17 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class StaleDevicesException extends NonSuccessfulResponseCodeException {
private final StaleDevices staleDevices;
public StaleDevicesException(StaleDevices staleDevices) {
this.staleDevices = staleDevices;
}
public StaleDevices getStaleDevices() {
return staleDevices;
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.GcmRefreshJob;
import org.thoughtcrime.securesms.jobs.JobLogger;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* Will be called once when the TextSecure process is created.
*
* We're using this as an insertion point to patch up the Android PRNG disaster,
* to initialize the job manager, and to check for GCM registration freshness.
*
* @author Moxie Marlinspike
*/
public class ApplicationContext extends Application {
private JobManager jobManager;
public static ApplicationContext getInstance(Context context) {
return (ApplicationContext)context.getApplicationContext();
}
@Override
public void onCreate() {
initializeRandomNumberFix();
initializeJobManager();
initializeGcmCheck();
}
public JobManager getJobManager() {
return jobManager;
}
private void initializeRandomNumberFix() {
PRNGFixes.apply();
}
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);
}
private void initializeGcmCheck() {
if (TextSecurePreferences.isPushRegistered(this) &&
TextSecurePreferences.getGcmRegistrationId(this) == null)
{
this.jobManager.addJob(new GcmRefreshJob());
}
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.app.Application;
import org.thoughtcrime.securesms.crypto.PRNGFixes;
/**
* Will be called once when the TextSecure process is created.
*
* We're using this as an insertion point to patch up the Android PRNG disaster.
*
* @author Moxie Marlinspike
*/
public class ApplicationListener extends Application {
@Override
public void onCreate() {
PRNGFixes.apply();
}
}

View File

@ -61,7 +61,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.AuthorizationFailedException;
import org.whispersystems.textsecure.push.exceptions.AuthorizationFailedException;
import org.whispersystems.textsecure.push.PushServiceSocket;
import java.io.IOException;

View File

@ -35,9 +35,9 @@ import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.ExpectationFailedException;
import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.push.exceptions.RateLimitException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import org.whispersystems.textsecure.util.Util;

View File

@ -9,8 +9,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.GcmRegistrationService;
import org.thoughtcrime.securesms.service.PreKeyService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
@ -122,8 +120,6 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
final ConversationParameters parameters = getConversationParameters();
final Intent intent;
scheduleRefreshActions();
if (isShareAction()) intent = getShareIntent(parameters);
else if (parameters.recipients != null) intent = getConversationIntent(parameters);
else intent = getConversationListIntent();
@ -173,20 +169,20 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
return intent;
}
private void scheduleRefreshActions() {
if (TextSecurePreferences.isPushRegistered(this) &&
TextSecurePreferences.getGcmRegistrationId(this) == null)
{
Intent intent = new Intent(this, GcmRegistrationService.class);
startService(intent);
}
if (TextSecurePreferences.isPushRegistered(this) &&
!TextSecurePreferences.isSignedPreKeyRegistered(this))
{
PreKeyService.initiateCreateSigned(this, masterSecret);
}
}
// private void scheduleRefreshActions() {
// if (TextSecurePreferences.isPushRegistered(this) &&
// TextSecurePreferences.getGcmRegistrationId(this) == null)
// {
// Intent intent = new Intent(this, GcmRegistrationService.class);
// startService(intent);
// }
//
// if (TextSecurePreferences.isPushRegistered(this) &&
// !TextSecurePreferences.isSignedPreKeyRegistered(this))
// {
// PreKeyService.initiateCreateSigned(this, masterSecret);
// }
// }
private int getApplicationState() {
if (!MasterSecretUtil.isPassphraseInitialized(this))

View File

@ -55,7 +55,9 @@ public class DatabaseFactory {
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
private static final int INTRODUCED_GROUP_DATABASE_VERSION = 11;
private static final int INTRODUCED_PUSH_FIX_VERSION = 12;
private static final int DATABASE_VERSION = 12;
private static final int INTRODUCED_DELIVERY_RECEIPTS = 13;
private static final int DATABASE_VERSION = 13;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -702,6 +704,13 @@ public class DatabaseFactory {
db.execSQL("DROP TABLE push_backup;");
}
if (oldVersion < INTRODUCED_DELIVERY_RECEIPTS) {
db.execSQL("ALTER TABLE sms ADD COLUMN delivery_receipt_count INTEGER DEFAULT 0;");
db.execSQL("ALTER TABLE mms ADD COLUMN delivery_receipt_count INTEGER DEFAULT 0;");
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS sms_date_sent_index ON sms (date_sent);");
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS mms_date_sent_index ON mms (date);");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

@ -28,6 +28,8 @@ import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduHeaders;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
public class MmsAddressDatabase extends Database {
@ -106,6 +108,25 @@ public class MmsAddressDatabase extends Database {
}
}
public List<String> getAddressesForId(long messageId) {
List<String> results = new LinkedList<String>();
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {messageId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
results.add(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)));
}
} finally {
if (cursor != null)
cursor.close();
}
return results;
}
public void deleteAddressesForId(long messageId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {messageId+""});

View File

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Trimmer;
import org.whispersystems.textsecure.util.Util;
@ -122,13 +123,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + RETRIEVE_STATUS + " INTEGER, " +
RETRIEVE_TEXT + " TEXT, " + RETRIEVE_TEXT_CS + " INTEGER, " + READ_STATUS + " INTEGER, " +
CONTENT_CLASS + " INTEGER, " + RESPONSE_TEXT + " TEXT, " + DELIVERY_TIME + " INTEGER, " +
DELIVERY_REPORT + " INTEGER);";
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + DELIVERY_REPORT + " INTEGER);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
"CREATE INDEX IF NOT EXISTS mms_read_index ON " + TABLE_NAME + " (" + READ + ");",
"CREATE INDEX IF NOT EXISTS mms_read_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + THREAD_ID + ");",
"CREATE INDEX IF NOT EXISTS mms_message_box_index ON " + TABLE_NAME + " (" + MESSAGE_BOX + ");"
"CREATE INDEX IF NOT EXISTS mms_message_box_index ON " + TABLE_NAME + " (" + MESSAGE_BOX + ");",
"CREATE INDEX IF NOT EXISTS mms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ");"
};
private static final String[] MMS_PROJECTION = new String[] {
@ -139,6 +141,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT,
DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
RECEIPT_COUNT
};
public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor();
@ -166,6 +169,43 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return 0;
}
public void incrementDeliveryReceiptCount(String address, long timestamp) {
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID}, DATE_SENT + " = ?", new String[] {String.valueOf(timestamp / 1000)}, null, null, null, null);
while (cursor.moveToNext()) {
List<String> addresses = addressDatabase.getAddressesForId(cursor.getLong(cursor.getColumnIndexOrThrow(ID)));
for (String storedAddress : addresses) {
try {
String ourAddress = org.thoughtcrime.securesms.util.Util.canonicalizeNumber(context, address);
String theirAddress = org.thoughtcrime.securesms.util.Util.canonicalizeNumber(context, storedAddress);
if (ourAddress.equals(theirAddress)) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
database.execSQL("UPDATE " + TABLE_NAME + " SET " +
RECEIPT_COUNT + " = " + RECEIPT_COUNT + " + 1 WHERE " + ID + " = ?",
new String[] {String.valueOf(id)});
notifyConversationListeners(threadId);
}
} catch (InvalidNumberException e) {
Log.w("MmsDatabase", e);
}
}
}
} finally {
if (cursor != null)
cursor.close();
}
}
public long getThreadIdForMessage(long id) {
String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?";
String[] sqlArgs = new String[] {id+""};
@ -418,6 +458,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
PduHeaders headers = getHeadersFromCursor(cursor);
addr.getAddressesForId(messageId, headers);
@ -433,7 +474,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
Log.w("MmsDatabase", e);
}
requests[i++] = new SendReq(headers, body, messageId, outboxType);
requests[i++] = new SendReq(headers, body, messageId, outboxType, timestamp);
}
return requests;
@ -902,6 +943,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
long messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_SIZE));
long expiry = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRY));
int status = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.RECEIPT_COUNT));
byte[]contentLocationBytes = null;
byte[]transactionIdBytes = null;
@ -914,7 +956,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return new NotificationMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
addressDeviceId, dateSent, dateReceived, threadId,
addressDeviceId, dateSent, dateReceived, receiptCount, threadId,
contentLocationBytes, messageSize, expiry, status,
transactionIdBytes, mailbox);
}
@ -927,6 +969,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.RECEIPT_COUNT));
DisplayRecord.Body body = getBody(cursor);
int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT));
Recipients recipients = getRecipientsFor(address);
@ -934,8 +977,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, id);
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
addressDeviceId, dateSent, dateReceived, threadId, body,
slideDeck, partCount, box);
addressDeviceId, dateSent, dateReceived, receiptCount,
threadId, body, slideDeck, partCount, box);
}
private Recipients getRecipientsFor(String address) {

View File

@ -10,6 +10,7 @@ public interface MmsSmsColumns {
public static final String BODY = "body";
public static final String ADDRESS = "address";
public static final String ADDRESS_DEVICE_ID = "address_device_id";
public static final String RECEIPT_COUNT = "delivery_receipt_count";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;

View File

@ -23,8 +23,8 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.whispersystems.textsecure.crypto.MasterSecret;
import java.util.HashSet;
import java.util.Set;
@ -49,7 +49,7 @@ public class MmsSmsDatabase extends Database {
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, TRANSPORT};
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
@ -71,7 +71,7 @@ public class MmsSmsDatabase extends Database {
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, TRANSPORT};
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
@ -89,7 +89,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, TRANSPORT};
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.READ + " = 0";
@ -104,6 +104,11 @@ public class MmsSmsDatabase extends Database {
return count;
}
public void incrementDeliveryReceiptCount(String address, long timestamp) {
DatabaseFactory.getSmsDatabase(context).incrementDeliveryReceiptCount(address, timestamp);
DatabaseFactory.getMmsDatabase(context).incrementDeliveryReceiptCount(address, timestamp);
}
private Cursor queryTables(String[] projection, String selection, String order, String groupBy, String limit) {
String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -112,7 +117,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
TRANSPORT};
MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -121,7 +126,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
TRANSPORT};
MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
@ -140,6 +145,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.BODY);
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS);
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID);
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
@ -158,6 +164,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID);
smsColumnsPresent.add(MmsSmsColumns.READ);
smsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.SUBJECT);
smsColumnsPresent.add(SmsDatabase.DATE_SENT);

View File

@ -36,13 +36,18 @@ import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Trimmer;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.Util;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static org.thoughtcrime.securesms.util.Util.canonicalizeNumber;
/**
* Database for storage of SMS messages.
*
@ -66,13 +71,15 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " + PERSON + " INTEGER, " +
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
SUBJECT + " TEXT, " + BODY + " TEXT, " + SERVICE_CENTER + " TEXT);";
RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
SERVICE_CENTER + " TEXT);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
"CREATE INDEX IF NOT EXISTS sms_read_index ON " + TABLE_NAME + " (" + READ + ");",
"CREATE INDEX IF NOT EXISTS sms_read_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + THREAD_ID + ");",
"CREATE INDEX IF NOT EXISTS sms_type_index ON " + TABLE_NAME + " (" + TYPE + ");"
"CREATE INDEX IF NOT EXISTS sms_type_index ON " + TABLE_NAME + " (" + TYPE + ");",
"CREATE INDEX IF NOT EXISTS sms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ");"
};
private static final String[] MESSAGE_PROJECTION = new String[] {
@ -80,7 +87,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED,
DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT
};
public SmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
@ -240,6 +247,38 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE);
}
public void incrementDeliveryReceiptCount(String address, long timestamp) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, ADDRESS},
DATE_SENT + " = ?", new String[] {String.valueOf(timestamp)},
null, null, null, null);
while (cursor.moveToNext()) {
try {
String theirAddress = canonicalizeNumber(context, address);
String ourAddress = canonicalizeNumber(context, cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)));
if (ourAddress.equals(theirAddress)) {
database.execSQL("UPDATE " + TABLE_NAME +
" SET " + RECEIPT_COUNT + " = " + RECEIPT_COUNT + " + 1 WHERE " +
ID + " = ?",
new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))});
notifyConversationListeners(cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)));
}
} catch (InvalidNumberException e) {
Log.w("SmsDatabase", e);
}
}
} finally {
if (cursor != null)
cursor.close();
}
}
public void setMessagesRead(long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
@ -539,13 +578,14 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_SENT));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.RECEIPT_COUNT));
Recipients recipients = getRecipientsFor(address);
DisplayRecord.Body body = getBody(cursor);
return new SmsMessageRecord(context, messageId, body, recipients,
recipients.getPrimaryRecipient(),
addressDeviceId,
dateSent, dateReceived, type,
dateSent, dateReceived, receiptCount, type,
threadId, status);
}

View File

@ -43,12 +43,13 @@ public class MediaMmsMessageRecord extends MessageRecord {
public MediaMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId,
long dateSent, long dateReceived, long threadId, Body body,
long dateSent, long dateReceived, int deliveredCount,
long threadId, Body body,
ListenableFutureTask<SlideDeck> slideDeck,
int partCount, long mailbox)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox);
dateSent, dateReceived, threadId, deliveredCount, DELIVERY_STATUS_NONE, mailbox);
this.context = context.getApplicationContext();
this.partCount = partCount;

View File

@ -48,17 +48,19 @@ public abstract class MessageRecord extends DisplayRecord {
private final int recipientDeviceId;
private final long id;
private final int deliveryStatus;
private final int receiptCount;
MessageRecord(Context context, long id, Body body, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId,
long dateSent, long dateReceived,
long threadId, int deliveryStatus, long type)
long dateSent, long dateReceived, long threadId,
int deliveryStatus, int receiptCount, long type)
{
super(context, body, recipients, dateSent, dateReceived, threadId, type);
this.id = id;
this.individualRecipient = individualRecipient;
this.recipientDeviceId = recipientDeviceId;
this.deliveryStatus = deliveryStatus;
this.receiptCount = receiptCount;
}
public abstract boolean isMms();
@ -110,7 +112,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
public boolean isDelivered() {
return getDeliveryStatus() == DELIVERY_STATUS_RECEIVED;
return getDeliveryStatus() == DELIVERY_STATUS_RECEIVED || receiptCount > 0;
}
public boolean isPush() {

View File

@ -42,12 +42,12 @@ public class NotificationMmsMessageRecord extends MessageRecord {
public NotificationMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId,
long dateSent, long dateReceived, long threadId,
byte[] contentLocation, long messageSize, long expiry,
int status, byte[] transactionId, long mailbox)
long dateSent, long dateReceived, int receiptCount,
long threadId, byte[] contentLocation, long messageSize,
long expiry, int status, byte[] transactionId, long mailbox)
{
super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox);
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, receiptCount, mailbox);
this.contentLocation = contentLocation;
this.messageSize = messageSize;

View File

@ -43,11 +43,12 @@ public class SmsMessageRecord extends MessageRecord {
Recipient individualRecipient,
int recipientDeviceId,
long dateSent, long dateReceived,
int receiptCount,
long type, long threadId,
int status)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, getGenericDeliveryStatus(status), type);
dateSent, dateReceived, threadId, receiptCount, getGenericDeliveryStatus(status), type);
}
public long getType() {

View File

@ -6,7 +6,10 @@ 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.libaxolotl.InvalidVersionException;
@ -31,38 +34,47 @@ public class GcmBroadcastReceiver extends BroadcastReceiver {
if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {
Log.w(TAG, "GCM message...");
try {
String data = intent.getStringExtra("message");
if (Util.isEmpty(data))
return;
if (!TextSecurePreferences.isPushRegistered(context)) {
Log.w(TAG, "Not push registered!");
return;
}
String sessionKey = TextSecurePreferences.getSignalingKey(context);
IncomingEncryptedPushMessage encryptedMessage = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encryptedMessage.getIncomingPushMessage();
if (!isActiveNumber(context, message.getSource())) {
Directory directory = Directory.getInstance(context);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(message.getSource());
directory.setNumber(contactTokenDetails, true);
}
Intent service = new Intent(context, SendReceiveService.class);
service.setAction(SendReceiveService.RECEIVE_PUSH_ACTION);
service.putExtra("message", message);
context.startService(service);
} catch (IOException e) {
Log.w(TAG, e);
} catch (InvalidVersionException e) {
Log.w(TAG, e);
if (!TextSecurePreferences.isPushRegistered(context)) {
Log.w(TAG, "Not push registered!");
return;
}
String messageData = intent.getStringExtra("message");
String receiptData = intent.getStringExtra("receipt");
if (!Util.isEmpty(messageData)) handleReceivedMessage(context, messageData);
else if (!Util.isEmpty(receiptData)) handleReceivedMessage(context, receiptData);
}
}
private void handleReceivedMessage(Context context, String data) {
try {
String sessionKey = TextSecurePreferences.getSignalingKey(context);
IncomingEncryptedPushMessage encrypted = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encrypted.getIncomingPushMessage();
if (!isActiveNumber(context, message.getSource())) {
Directory directory = Directory.getInstance(context);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(message.getSource());
directory.setNumber(contactTokenDetails, true);
}
Intent receiveService = new Intent(context, SendReceiveService.class);
receiveService.setAction(SendReceiveService.RECEIVE_PUSH_ACTION);
receiveService.putExtra("message", message);
context.startService(receiveService);
if (!message.isReceipt()) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.addJob(new DeliveryReceiptJob(message.getSource(), message.getTimestampMillis(),
message.getRelay()));
}
} catch (IOException e) {
Log.w(TAG, e);
} catch (InvalidVersionException e) {
Log.w(TAG, e);
}
}

View File

@ -0,0 +1,22 @@
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);
}
}
}

View File

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import com.path.android.jobqueue.Job;
import com.path.android.jobqueue.Params;
public abstract class ContextJob extends Job {
transient protected Context context;
protected ContextJob(Params params) {
super(params);
}
public void setContext(Context context) {
this.context = context;
}
protected Context getContext() {
return context;
}
}

View File

@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.jobs;
import android.util.Log;
import com.path.android.jobqueue.Params;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.push.exceptions.PushNetworkException;
public class DeliveryReceiptJob extends ContextJob {
private static final String TAG = DeliveryReceiptJob.class.getSimpleName();
private final String destination;
private final long timestamp;
private final String relay;
public DeliveryReceiptJob(String destination, long timestamp, String relay) {
super(new Params(Priorities.HIGH).requireNetwork().persist());
this.destination = destination;
this.timestamp = timestamp;
this.relay = relay;
}
@Override
public void onAdded() {}
@Override
public void onRun() throws Throwable {
Log.w("DeliveryReceiptJob", "Sending delivery receipt...");
PushServiceSocket socket = PushServiceSocketFactory.create(context);
socket.sendReceipt(destination, timestamp, relay);
}
@Override
protected void onCancel() {
Log.w(TAG, "Failed to send receipt after retry exhausted!");
}
@Override
protected boolean shouldReRunOnThrowable(Throwable throwable) {
Log.w(TAG, throwable);
if (throwable instanceof NonSuccessfulResponseCodeException) return false;
if (throwable instanceof PushNetworkException) return true;
return false;
}
@Override
protected int getRetryLimit() {
return 50;
}
}

View File

@ -0,0 +1,61 @@
package org.thoughtcrime.securesms.jobs;
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.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 {
private static final String TAG = GcmRefreshJob.class.getSimpleName();
public static final String REGISTRATION_ID = "312334754206";
public GcmRefreshJob() {
super(new Params(Priorities.NORMAL).requireNetwork());
}
@Override
public void onAdded() {}
@Override
public void onRun() throws Exception {
String registrationId = TextSecurePreferences.getGcmRegistrationId(context);
if (registrationId == null) {
Log.w(TAG, "GCM registrationId expired, reregistering...");
int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
if (result != ConnectionResult.SUCCESS) {
Toast.makeText(context, "Unable to register with GCM!", Toast.LENGTH_LONG).show();
}
String gcmId = GoogleCloudMessaging.getInstance(context).register(REGISTRATION_ID);
PushServiceSocket socket = PushServiceSocketFactory.create(context);
socket.registerGcmId(gcmId);
TextSecurePreferences.setGcmRegistrationId(context, gcmId);
}
}
@Override
protected void onCancel() {
Log.w(TAG, "GCM reregistration failed after retry attempt exhaustion!");
}
@Override
protected boolean shouldReRunOnThrowable(Throwable throwable) {
if (throwable instanceof NonSuccessfulResponseCodeException) return false;
return true;
}
}

View File

@ -0,0 +1,27 @@
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));
}
}

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.jobs;
public class Priorities {
public static final int NORMAL = 500;
public static final int HIGH = 1000;
}

View File

@ -6,7 +6,6 @@ import android.content.Intent;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.Release;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingPartDatabase;
import org.thoughtcrime.securesms.database.PartDatabase;
@ -16,7 +15,7 @@ import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.NotFoundException;
import org.whispersystems.textsecure.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Base64;

View File

@ -50,6 +50,8 @@ import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageCo
public class PushReceiver {
private static final String TAG = PushReceiver.class.getSimpleName();
public static final int RESULT_OK = 0;
public static final int RESULT_NO_SESSION = 1;
public static final int RESULT_DECRYPT_FAILED = 2;
@ -97,7 +99,9 @@ public class PushReceiver {
if (message.isSecureMessage()) handleReceivedSecureMessage(masterSecret, message);
else if (message.isPreKeyBundle()) handleReceivedPreKeyBundle(masterSecret, message);
else handleReceivedMessage(masterSecret, message, false);
else if (message.isReceipt()) handleReceivedReceipt(message);
else if (message.isPlaintext()) handleReceivedMessage(masterSecret, message, false);
else Log.w(TAG, "Received push of unknown type!");
}
private void handleReceivedSecureMessage(MasterSecret masterSecret, IncomingPushMessage message) {
@ -130,21 +134,21 @@ public class PushReceiver {
handleReceivedMessage(masterSecret, bundledMessage, true);
} catch (InvalidVersionException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
handleReceivedCorruptedKey(masterSecret, message, true);
} catch (InvalidKeyException | InvalidKeyIdException | InvalidMessageException |
RecipientFormattingException | LegacyMessageException e)
{
Log.w("PushReceiver", e);
Log.w(TAG, e);
handleReceivedCorruptedKey(masterSecret, message, false);
} catch (DuplicateMessageException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
handleReceivedDuplicateMessage(message);
} catch (NoSessionException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
handleReceivedMessageForNoSession(masterSecret, message);
} catch (UntrustedIdentityException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
String encoded = Base64.encodeBytes(message.getBody());
IncomingTextMessage textMessage = new IncomingTextMessage(message, encoded, null);
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
@ -163,24 +167,31 @@ public class PushReceiver {
PushMessageContent messageContent = PushMessageContent.parseFrom(message.getBody());
if (secure && (messageContent.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0) {
Log.w("PushReceiver", "Received end session message...");
Log.w(TAG, "Received end session message...");
handleEndSessionMessage(masterSecret, message, messageContent);
} else if (messageContent.hasGroup() && messageContent.getGroup().getType().getNumber() != Type.DELIVER_VALUE) {
Log.w("PushReceiver", "Received push group message...");
Log.w(TAG, "Received push group message...");
groupReceiver.process(masterSecret, message, messageContent, secure);
} else if (messageContent.getAttachmentsCount() > 0) {
Log.w("PushReceiver", "Received push media message...");
Log.w(TAG, "Received push media message...");
handleReceivedMediaMessage(masterSecret, message, messageContent, secure);
} else {
Log.w("PushReceiver", "Received push text message...");
Log.w(TAG, "Received push text message...");
handleReceivedTextMessage(masterSecret, message, messageContent, secure);
}
} catch (InvalidProtocolBufferException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
handleReceivedCorruptedMessage(masterSecret, message, secure);
}
}
private void handleReceivedReceipt(IncomingPushMessage message)
{
Log.w("PushReceiver", String.format("Received receipt: (XXXXX, %d)", message.getTimestampMillis()));
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(message.getSource(),
message.getTimestampMillis());
}
private void handleEndSessionMessage(MasterSecret masterSecret,
IncomingPushMessage message,
PushMessageContent messageContent)
@ -200,7 +211,7 @@ public class PushReceiver {
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, messageAndThreadId.second);
} catch (RecipientFormattingException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
}
}
@ -231,7 +242,7 @@ public class PushReceiver {
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} catch (MmsException e) {
Log.w("PushReceiver", e);
Log.w(TAG, e);
// XXX
}
}
@ -267,7 +278,7 @@ public class PushReceiver {
}
private void handleReceivedDuplicateMessage(IncomingPushMessage message) {
Log.w("PushReceiver", "Received duplicate message: " + message.getSource() + " , " + message.getSourceDevice());
Log.w(TAG, "Received duplicate message: " + message.getSource() + " , " + message.getSourceDevice());
}
private void handleReceivedCorruptedKey(MasterSecret masterSecret,

View File

@ -23,7 +23,7 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.PreKeyUtil;
import org.whispersystems.textsecure.push.ExpectationFailedException;
import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Util;

View File

@ -43,7 +43,7 @@ import org.whispersystems.textsecure.crypto.AttachmentCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.TransportDetails;
import org.whispersystems.textsecure.push.MismatchedDevices;
import org.whispersystems.textsecure.push.MismatchedDevicesException;
import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.push.OutgoingPushMessage;
import org.whispersystems.textsecure.push.OutgoingPushMessageList;
import org.whispersystems.textsecure.push.PushAddress;
@ -54,7 +54,7 @@ import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.PushTransportDetails;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.StaleDevicesException;
import org.whispersystems.textsecure.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionUtil;
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
@ -92,7 +92,7 @@ public class PushTransport extends BaseTransport {
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = getPlaintextMessage(message);
deliver(socket, recipient, threadId, plaintext);
deliver(socket, recipient, message.getDateSent(), threadId, plaintext);
if (message.isEndSession()) {
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
@ -129,7 +129,7 @@ public class PushTransport extends BaseTransport {
for (Recipient recipient : recipients.getRecipientsList()) {
try {
deliver(socket, recipient, threadId, plaintext);
deliver(socket, recipient, message.getSentTimestamp(), threadId, plaintext);
} catch (UntrustedIdentityException e) {
Log.w("PushTransport", e);
untrustedIdentities.add(e);
@ -144,13 +144,14 @@ public class PushTransport extends BaseTransport {
}
}
private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext)
private void deliver(PushServiceSocket socket, Recipient recipient, long timestamp,
long threadId, byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException
{
for (int i=0;i<3;i++) {
try {
OutgoingPushMessageList messages = getEncryptedMessages(socket, threadId,
recipient, plaintext);
OutgoingPushMessageList messages = getEncryptedMessages(socket, threadId, recipient,
timestamp, plaintext);
socket.sendMessage(messages);
return;
@ -300,7 +301,8 @@ public class PushTransport extends BaseTransport {
}
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, long threadId,
Recipient recipient, byte[] plaintext)
Recipient recipient, long timestamp,
byte[] plaintext)
throws IOException, InvalidNumberException, UntrustedIdentityException
{
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
@ -319,7 +321,7 @@ public class PushTransport extends BaseTransport {
messages.add(new OutgoingPushMessage(device, body));
}
return new OutgoingPushMessageList(e164number, masterDevice.getRelay(), messages);
return new OutgoingPushMessageList(e164number, timestamp, masterDevice.getRelay(), messages);
}
private PushBody getEncryptedMessage(PushServiceSocket socket, long threadId,

View File

@ -25,6 +25,7 @@ public class SendReq extends MultimediaMessagePdu {
private static final String TAG = "SendReq";
private long databaseMessageId;
private long messageBox;
private long timestamp;
public SendReq() {
super();
@ -90,11 +91,12 @@ public class SendReq extends MultimediaMessagePdu {
super(headers, body);
}
public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox)
public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox, long timestamp)
{
super(headers, body);
this.databaseMessageId = messageId;
this.messageBox = messageBox;
this.timestamp = timestamp;
}
public long getDatabaseMessageBox() {
@ -105,6 +107,10 @@ public class SendReq extends MultimediaMessagePdu {
return databaseMessageId;
}
public long getSentTimestamp() {
return timestamp;
}
/**
* Get Bcc value.
*