Support for incoming attachments.

1) Refactored MMS layer to use abstracted types.

2) Added support for retrieving attachment IDs.
This commit is contained in:
Moxie Marlinspike 2013-07-18 17:42:45 -07:00
parent 4bb337a3a0
commit 9287d413ac
23 changed files with 501 additions and 193 deletions

View File

@ -33,6 +33,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.GZIPInputStream;
/**
* Handles providing lookups, serializing, and deserializing the RedPhone directory.
@ -102,7 +103,30 @@ public class NumberFilter {
}
}
public synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
public synchronized void update(DirectoryDescriptor descriptor, File compressedData) {
try {
File uncompressed = File.createTempFile("directory", ".dat", context.getFilesDir());
FileInputStream fin = new FileInputStream (compressedData);
GZIPInputStream gin = new GZIPInputStream(fin);
FileOutputStream out = new FileOutputStream(uncompressed);
byte[] buffer = new byte[4096];
int read;
while ((read = gin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
out.close();
compressedData.delete();
update(uncompressed, descriptor.getCapacity(), descriptor.getHashCount(), descriptor.getVersion());
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
}
}
private synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
{
if (this.bloomFilter != null)
this.bloomFilter.delete();

View File

@ -1,41 +0,0 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class IncomingGcmMessage {
private String source;
private List<String> destinations;
private String messageText;
private List<String> attachments;
private long timestamp;
public IncomingGcmMessage(String source, List<String> destinations, String messageText, List<String> attachments, long timestamp) {
this.source = source;
this.destinations = destinations;
this.messageText = messageText;
this.attachments = attachments;
this.timestamp = timestamp;
}
public long getTimestampMillis() {
return timestamp;
}
public String getSource() {
return source;
}
public List<String> getAttachments() {
return attachments;
}
public String getMessageText() {
return messageText;
}
public List<String> getDestinations() {
return destinations;
}
}

View File

@ -0,0 +1,87 @@
package org.whispersystems.textsecure.push;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.LinkedList;
import java.util.List;
public class IncomingPushMessage implements Parcelable {
public static final Parcelable.Creator<IncomingPushMessage> CREATOR = new Parcelable.Creator<IncomingPushMessage>() {
@Override
public IncomingPushMessage createFromParcel(Parcel in) {
return new IncomingPushMessage(in);
}
@Override
public IncomingPushMessage[] newArray(int size) {
return new IncomingPushMessage[size];
}
};
private String source;
private List<String> destinations;
private String messageText;
private List<PushAttachmentPointer> attachments;
private long timestamp;
public IncomingPushMessage(String source, List<String> destinations, String messageText,
List<PushAttachmentPointer> attachments, long timestamp)
{
this.source = source;
this.destinations = destinations;
this.messageText = messageText;
this.attachments = attachments;
this.timestamp = timestamp;
}
public IncomingPushMessage(Parcel in) {
this.destinations = new LinkedList<String>();
this.attachments = new LinkedList<PushAttachmentPointer>();
this.source = in.readString();
in.readStringList(destinations);
this.messageText = in.readString();
in.readList(attachments, PushAttachmentPointer.class.getClassLoader());
this.timestamp = in.readLong();
}
public long getTimestampMillis() {
return timestamp;
}
public String getSource() {
return source;
}
public List<PushAttachmentPointer> getAttachments() {
return attachments;
}
public String getMessageText() {
return messageText;
}
public List<String> getDestinations() {
return destinations;
}
public boolean hasAttachments() {
return getAttachments() != null && !getAttachments().isEmpty();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(source);
dest.writeStringList(destinations);
dest.writeString(messageText);
dest.writeList(attachments);
dest.writeLong(timestamp);
}
}

View File

@ -5,13 +5,13 @@ import java.util.List;
public class OutgoingPushMessage {
private List<String> destinations;
private String messageText;
private List<String> attachments;
private List<String> destinations;
private String messageText;
private List<PushAttachmentPointer> attachments;
public OutgoingPushMessage(String destination, String messageText) {
this.destinations = new LinkedList<String>();
this.attachments = new LinkedList<String>();
this.attachments = new LinkedList<PushAttachmentPointer>();
this.messageText = messageText;
this.destinations.add(destination);
}
@ -19,11 +19,11 @@ public class OutgoingPushMessage {
public OutgoingPushMessage(List<String> destinations, String messageText) {
this.destinations = destinations;
this.messageText = messageText;
this.attachments = new LinkedList<String>();
this.attachments = new LinkedList<PushAttachmentPointer>();
}
public OutgoingPushMessage(List<String> destinations, String messageText,
List<String> attachments)
List<PushAttachmentPointer> attachments)
{
this.destinations = destinations;
this.messageText = messageText;
@ -38,7 +38,7 @@ public class OutgoingPushMessage {
return messageText;
}
public List<String> getAttachments() {
public List<PushAttachmentPointer> getAttachments() {
return attachments;
}

View File

@ -1,13 +1,13 @@
package org.whispersystems.textsecure.push;
public class PushAttachment {
public class PushAttachmentData {
private final String contentType;
private final byte[] data;
public PushAttachment(String contentType, byte[] data) {
public PushAttachmentData(String contentType, byte[] data) {
this.contentType = contentType;
this.data = data;
this.data = data;
}
public String getContentType() {

View File

@ -0,0 +1,51 @@
package org.whispersystems.textsecure.push;
import android.os.Parcel;
import android.os.Parcelable;
public class PushAttachmentPointer implements Parcelable {
public static final Parcelable.Creator<PushAttachmentPointer> CREATOR = new Parcelable.Creator<PushAttachmentPointer>() {
@Override
public PushAttachmentPointer createFromParcel(Parcel in) {
return new PushAttachmentPointer(in);
}
@Override
public PushAttachmentPointer[] newArray(int size) {
return new PushAttachmentPointer[size];
}
};
private final String contentType;
private final String key;
public PushAttachmentPointer(String contentType, String key) {
this.contentType = contentType;
this.key = key;
}
public PushAttachmentPointer(Parcel in) {
this.contentType = in.readString();
this.key = in.readString();
}
public String getContentType() {
return contentType;
}
public String getKey() {
return key;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(contentType);
dest.writeString(key);
}
}

View File

@ -2,7 +2,7 @@ package org.whispersystems.textsecure.push;
import java.util.List;
public class GcmMessageResponse {
public class PushMessageResponse {
private List<String> success;
private List<String> failure;

View File

@ -23,8 +23,6 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
@ -46,11 +44,13 @@ public class PushServiceSocket {
private static final String MESSAGE_PATH = "/v1/messages/";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
private final Context context;
private final String localNumber;
private final String password;
private final TrustManagerFactory trustManagerFactory;
public PushServiceSocket(Context context, String localNumber, String password) {
this.context = context.getApplicationContext();
this.localNumber = localNumber;
this.password = password;
this.trustManagerFactory = initializeTrustManagerFactory(context);
@ -70,54 +70,55 @@ public class PushServiceSocket {
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
}
public void unregisterGcmId() throws IOException, RateLimitException {
public void unregisterGcmId() throws IOException {
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
}
public void sendMessage(String recipient, String messageText)
throws IOException, RateLimitException
throws IOException
{
OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText);
sendMessage(message);
}
public void sendMessage(List<String> recipients, String messageText)
throws IOException, RateLimitException
throws IOException
{
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText);
sendMessage(message);
}
public void sendMessage(List<String> recipients, String messageText,
List<PushAttachment> attachments)
throws IOException, RateLimitException
List<PushAttachmentData> attachments)
throws IOException
{
List<String> attachmentIds = sendAttachments(attachments);
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds);
List<PushAttachmentPointer> attachmentIds = sendAttachments(attachments);
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds);
sendMessage(message);
}
private void sendMessage(OutgoingPushMessage message) throws IOException, RateLimitException {
String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message));
GcmMessageResponse response = new Gson().fromJson(responseText, GcmMessageResponse.class);
private void sendMessage(OutgoingPushMessage message) throws IOException {
String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message));
PushMessageResponse response = new Gson().fromJson(responseText, PushMessageResponse.class);
if (response.getFailure().size() != 0)
throw new IOException("Got send failure: " + response.getFailure().get(0));
}
private List<String> sendAttachments(List<PushAttachment> attachments)
throws IOException, RateLimitException
private List<PushAttachmentPointer> sendAttachments(List<PushAttachmentData> attachments)
throws IOException
{
List<String> attachmentIds = new LinkedList<String>();
List<PushAttachmentPointer> attachmentIds = new LinkedList<PushAttachmentPointer>();
for (PushAttachment attachment : attachments) {
attachmentIds.add(sendAttachment(attachment));
for (PushAttachmentData attachment : attachments) {
attachmentIds.add(new PushAttachmentPointer(attachment.getContentType(),
sendAttachment(attachment)));
}
return attachmentIds;
}
private String sendAttachment(PushAttachment attachment) throws IOException, RateLimitException {
private String sendAttachment(PushAttachmentData attachment) throws IOException {
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, ""),
"GET", null, "Content-Location");
@ -130,34 +131,52 @@ public class PushServiceSocket {
uploadExternalFile("PUT", contentLocation, attachment.getData());
return new Gson().fromJson(response.second, AttachmentDescriptor.class).getId();
return new Gson().fromJson(response.second, AttachmentKey.class).getId();
}
public void retrieveDirectory(Context context ) {
public List<File> retrieveAttachments(List<PushAttachmentPointer> attachmentIds)
throws IOException
{
List<File> attachments = new LinkedList<File>();
for (PushAttachmentPointer attachmentId : attachmentIds) {
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, attachmentId.getKey()),
"GET", null, "Content-Location");
Log.w("PushServiceSocket", "Attachment: " + attachmentId.getKey() + " is at: " + response.first);
File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir());
downloadExternalFile(response.first, attachment);
attachments.add(attachment);
}
return attachments;
}
public Pair<DirectoryDescriptor, File> retrieveDirectory() {
try {
DirectoryDescriptor directoryDescriptor = new Gson().fromJson(makeRequest(DIRECTORY_PATH, "GET", null),
DirectoryDescriptor.class);
File directoryData = File.createTempFile("directory", ".dat", context.getFilesDir());
downloadExternalFile(directoryDescriptor.getUrl(), directoryData);
NumberFilter.getInstance(context).update(directoryData,
directoryDescriptor.getCapacity(),
directoryDescriptor.getHashCount(),
directoryDescriptor.getVersion());
return new Pair<DirectoryDescriptor, File>(directoryDescriptor, directoryData);
} catch (IOException ioe) {
Log.w("PushServiceSocket", ioe);
} catch (RateLimitException e) {
Log.w("PushServiceSocket", e);
return null;
}
}
private void downloadExternalFile(String url, File localDestination)
throws IOException
{
URL downloadUrl = new URL(url);
HttpsURLConnection connection = (HttpsURLConnection) downloadUrl.openConnection();
URL downloadUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestMethod("GET");
connection.setDoInput(true);
try {
@ -166,7 +185,7 @@ public class PushServiceSocket {
}
OutputStream output = new FileOutputStream(localDestination);
InputStream input = new GZIPInputStream(connection.getInputStream());
InputStream input = connection.getInputStream();
byte[] buffer = new byte[4096];
int read;
@ -175,6 +194,7 @@ public class PushServiceSocket {
}
output.close();
Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
} finally {
connection.disconnect();
}
@ -205,7 +225,7 @@ public class PushServiceSocket {
private Pair<String, String> makeRequestForResponseHeader(String urlFragment, String method,
String body, String responseHeader)
throws IOException, RateLimitException
throws IOException
{
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
String response = Util.readFully(connection.getInputStream());
@ -216,7 +236,7 @@ public class PushServiceSocket {
}
private String makeRequest(String urlFragment, String method, String body)
throws IOException, RateLimitException
throws IOException
{
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
String response = Util.readFully(connection.getInputStream());
@ -227,7 +247,7 @@ public class PushServiceSocket {
}
private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
throws IOException, RateLimitException
throws IOException
{
HttpURLConnection connection = getConnection(urlFragment, method);
@ -314,7 +334,7 @@ public class PushServiceSocket {
}
}
private class GcmRegistrationId {
private static class GcmRegistrationId {
private String gcmRegistrationId;
public GcmRegistrationId() {}
@ -324,10 +344,10 @@ public class PushServiceSocket {
}
}
private class AttachmentDescriptor {
private static class AttachmentKey {
private String id;
public AttachmentDescriptor(String id) {
public AttachmentKey(String id) {
this.id = id;
}

View File

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

View File

@ -9,6 +9,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.List;
public class Util {
public static boolean isEmpty(String value) {
@ -55,4 +57,18 @@ public class Util {
return new String(bout.toByteArray());
}
public static String join(Collection<String> list, String delimiter) {
StringBuilder result = new StringBuilder();
int i=0;
for (String item : list) {
result.append(item);
if (++i < list.size())
result.append(delimiter);
}
return result.toString();
}
}

View File

@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import java.io.IOException;
@ -361,9 +360,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
} catch (IOException e) {
Log.w("ApplicationPreferencesActivity", e);
return NETWORK_ERROR;
} catch (RateLimitException e) {
Log.w("ApplicationPreferencesActivity", e);
return NETWORK_ERROR;
}
}
}.execute();

View File

@ -452,12 +452,12 @@ public class RegistrationProgressActivity extends SherlockActivity {
PushServiceSocket socket = new PushServiceSocket(context, e164number, password);
socket.verifyAccount(code);
return SUCCESS;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
return RATE_LIMIT_ERROR;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
}
}
}.execute();
@ -539,12 +539,12 @@ public class RegistrationProgressActivity extends SherlockActivity {
socket.createAccount(true);
return SUCCESS;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
return RATE_LIMIT_EXCEEDED;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
}
}
}.execute();

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -212,7 +213,7 @@ public class DecryptingQueue {
RetrieveConf plaintextPdu = new RetrieveConf(plaintextGenericPdu.getPduHeaders(),
plaintextGenericPdu.getBody());
Log.w("DecryptingQueue", "Successfully decrypted MMS!");
database.insertSecureDecryptedMessageInbox(masterSecret, plaintextPdu, threadId);
database.insertSecureDecryptedMessageInbox(masterSecret, new IncomingMediaMessage(plaintextPdu), threadId);
database.delete(messageId);
} catch (RecipientFormattingException rfe) {
Log.w("DecryptingQueue", rfe);

View File

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.model.DisplayRecord;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.TextSlide;
@ -59,11 +60,9 @@ import ws.com.google.android.mms.InvalidHeaderValueException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.RetrieveConf;
import ws.com.google.android.mms.pdu.SendReq;
// XXXX Clean up MMS efficiency:
@ -177,27 +176,23 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
}
}
private long getThreadIdFor(RetrieveConf retrieved) throws RecipientFormattingException {
private long getThreadIdFor(IncomingMediaMessage retrieved) throws RecipientFormattingException {
try {
PduHeaders headers = retrieved.getPduHeaders();
Set<String> group = new HashSet<String>();
EncodedStringValue encodedFrom = retrieved.getFrom();
EncodedStringValue encodedFrom = headers.getEncodedStringValue(PduHeaders.FROM);
group.add(new String(encodedFrom.getTextString(), CharacterSets.MIMENAME_ISO_8859_1));
EncodedStringValue[] encodedCcList = retrieved.getCc();
EncodedStringValue[] encodedCcList = headers.getEncodedStringValues(PduHeaders.CC);
if (encodedCcList != null) {
for (EncodedStringValue encodedCc : encodedCcList) {
group.add(new String(encodedCc.getTextString(), CharacterSets.MIMENAME_ISO_8859_1));
}
}
StringBuilder sb = new StringBuilder();
for (String recipient : group) {
sb.append(recipient);
sb.append(",");
}
Recipients recipients = RecipientFactory.getRecipientsFromString(context, sb.toString(), false);
String recipientsList = Util.join(group, ",");
Recipients recipients = RecipientFactory.getRecipientsFromString(context, recipientsList, false);
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
@ -280,7 +275,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
ContentValues contentValues = new ContentValues();
contentValues.put(READ, 1);
database.update(TABLE_NAME, contentValues, THREAD_ID + " = ?", new String[] {threadId+""});
database.update(TABLE_NAME, contentValues, THREAD_ID + " = ?", new String[] {threadId + ""});
}
public void setAllMessagesRead() {
@ -359,7 +354,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return new Reader(masterSecret, cursor);
}
private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved,
String contentLocation, long threadId, long mailbox)
throws MmsException
{
@ -367,11 +362,13 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
ContentValues contentValues = getContentValuesFromHeader(headers);
boolean unread = Util.isDefaultSmsProvider(context) || ((mailbox & Types.SECURE_MESSAGE_BIT) != 0);
if (!org.thoughtcrime.securesms.util.Util.isEmpty(retrieved.getCc())) {
if (threadId == -1 || retrieved.isGroupMessage()) {
try {
threadId = getThreadIdFor(retrieved);
} catch (RecipientFormattingException e) {
Log.w("MmsDatabase", e);
if (threadId == -1)
throw new MmsException(e);
}
}
@ -382,10 +379,12 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
contentValues.put(DATE_RECEIVED, System.currentTimeMillis() / 1000);
contentValues.put(READ, unread ? 0 : 1);
if (!contentValues.containsKey(DATE_SENT))
if (!contentValues.containsKey(DATE_SENT)) {
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED));
}
long messageId = insertMediaMessage(masterSecret, retrieved, contentValues);
long messageId = insertMediaMessage(masterSecret, retrieved.getPduHeaders(),
retrieved.getBody(), contentValues);
if (unread) {
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
@ -398,7 +397,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return new Pair<Long, Long>(messageId, threadId);
}
public Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
public Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret,
IncomingMediaMessage retrieved,
String contentLocation, long threadId)
throws MmsException
{
@ -406,7 +406,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT);
}
public Pair<Long, Long> insertSecureMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
public Pair<Long, Long> insertSecureMessageInbox(MasterSecret masterSecret,
IncomingMediaMessage retrieved,
String contentLocation, long threadId)
throws MmsException
{
@ -415,7 +416,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
}
public Pair<Long, Long> insertSecureDecryptedMessageInbox(MasterSecret masterSecret,
RetrieveConf retrieved,
IncomingMediaMessage retrieved,
long threadId)
throws MmsException
{
@ -425,11 +426,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
public Pair<Long, Long> insertMessageInbox(NotificationInd notification) {
try {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
PduHeaders headers = notification.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
long threadId = getThreadIdFor(notification);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
long threadId = getThreadIdFor(notification);
PduHeaders headers = notification.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
Log.w("MmsDatabse", "Message received type: " + headers.getOctet(PduHeaders.MESSAGE_TYPE));
@ -486,21 +487,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.remove(ADDRESS);
long messageId = insertMediaMessage(masterSecret, sendRequest, contentValues);
long messageId = insertMediaMessage(masterSecret, sendRequest.getPduHeaders(),
sendRequest.getBody(), contentValues);
Trimmer.trimThread(context, threadId);
return messageId;
}
private long insertMediaMessage(MasterSecret masterSecret,
MultimediaMessagePdu message,
PduHeaders headers,
PduBody body,
ContentValues contentValues)
throws MmsException
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
PartDatabase partsDatabase = getPartDatabase(masterSecret);
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
PduBody body = message.getBody();
if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) {
String messageText = PartParser.getMessageText(body);
@ -515,7 +517,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
long messageId = db.insert(TABLE_NAME, null, contentValues);
addressDatabase.insertAddressesForId(messageId, message.getPduHeaders());
addressDatabase.insertAddressesForId(messageId, headers);
partsDatabase.insertParts(messageId, body);
notifyConversationListeners(contentValues.getAsLong(THREAD_ID));

View File

@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.push.IncomingGcmMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.Util;
@ -33,8 +33,6 @@ public class GcmIntentService extends GCMBaseIntentService {
getGcmSocket(context).registerGcmId(registrationId);
} catch (IOException e) {
Log.w("GcmIntentService", e);
} catch (RateLimitException e) {
Log.w("GcmIntentService", e);
}
}
}
@ -45,22 +43,30 @@ public class GcmIntentService extends GCMBaseIntentService {
getGcmSocket(context).unregisterGcmId();
} catch (IOException ioe) {
Log.w("GcmIntentService", ioe);
} catch (RateLimitException e) {
Log.w("GcmIntentService", e);
}
}
@Override
protected void onMessage(Context context, Intent intent) {
Log.w("GcmIntentService", "Got GCM message!");
String data = intent.getStringExtra("message");
Log.w("GcmIntentService", "GCM message: " + data);
if (Util.isEmpty(data))
return;
IncomingGcmMessage message = new Gson().fromJson(data, IncomingGcmMessage.class);
IncomingPushMessage message = new Gson().fromJson(data, IncomingPushMessage.class);
if (!message.hasAttachments()) handleIncomingTextMessage(context, message);
else handleIncomingMediaMessage(context, message);
}
@Override
protected void onError(Context context, String s) {
Log.w("GcmIntentService", "GCM Error: " + s);
}
private void handleIncomingTextMessage(Context context, IncomingPushMessage message) {
ArrayList<IncomingTextMessage> messages = new ArrayList<IncomingTextMessage>();
messages.add(new IncomingTextMessage(message));
@ -70,9 +76,11 @@ public class GcmIntentService extends GCMBaseIntentService {
context.startService(receivedIntent);
}
@Override
protected void onError(Context context, String s) {
Log.w("GcmIntentService", "GCM Error: " + s);
private void handleIncomingMediaMessage(Context context, IncomingPushMessage message) {
Intent receivedIntent = new Intent(context, SendReceiveService.class);
receivedIntent.setAction(SendReceiveService.RECEIVE_PUSH_MMS_ACTION);
receivedIntent.putExtra("media_message", message);
context.startService(receivedIntent);
}
private PushServiceSocket getGcmSocket(Context context) {

View File

@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.mms;
import android.util.Log;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushAttachmentPointer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.RetrieveConf;
public class IncomingMediaMessage {
private final PduHeaders headers;
private final PduBody body;
public IncomingMediaMessage(RetrieveConf retreived) {
this.headers = retreived.getPduHeaders();
this.body = retreived.getBody();
}
public IncomingMediaMessage(String localNumber, IncomingPushMessage message, List<File> attachments)
throws IOException
{
this.headers = new PduHeaders();
this.body = new PduBody();
this.headers.setEncodedStringValue(new EncodedStringValue(message.getSource()), PduHeaders.FROM);
this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO);
for (String destination : message.getDestinations()) {
if (!destination.equals(localNumber)) {
this.headers.appendEncodedStringValue(new EncodedStringValue(destination), PduHeaders.CC);
}
}
this.headers.setLongInteger(message.getTimestampMillis() / 1000, PduHeaders.DATE);
if (message.getMessageText() != null && message.getMessageText().length() > 0) {
PduPart text = new PduPart();
text.setData(message.getMessageText().getBytes());
text.setContentType("text/plain".getBytes(CharacterSets.MIMENAME_ISO_8859_1));
body.addPart(text);
}
Iterator<PushAttachmentPointer> descriptors = message.getAttachments().iterator();
if (attachments != null) {
for (File attachment : attachments) {
PduPart media = new PduPart();
FileInputStream fin = new FileInputStream(attachment);
byte[] data = Util.readFully(fin);
PushAttachmentPointer descriptor = descriptors.next();
Log.w("IncomingMediaMessage", "Adding part: " + descriptor.getContentType() + " with length: " + data.length);
media.setContentType(descriptor.getContentType().getBytes(CharacterSets.MIMENAME_ISO_8859_1));
media.setData(data);
body.addPart(media);
}
}
}
public PduHeaders getPduHeaders() {
return headers;
}
public PduBody getBody() {
return body;
}
public boolean isGroupMessage() {
return !Util.isEmpty(headers.getEncodedStringValues(PduHeaders.CC));
}
}

View File

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsDownloadHelper;
import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException;
@ -177,11 +178,13 @@ public class MmsDownloader {
long messageId, long threadId, RetrieveConf retrieved)
throws MmsException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage message = new IncomingMediaMessage(retrieved);
Pair<Long, Long> messageAndThreadId;
if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) {
messageAndThreadId = database.insertSecureMessageInbox(masterSecret, retrieved,
messageAndThreadId = database.insertSecureMessageInbox(masterSecret, message,
contentLocation, threadId);
if (masterSecret != null)
@ -189,7 +192,7 @@ public class MmsDownloader {
messageAndThreadId.second, retrieved);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, retrieved,
messageAndThreadId = database.insertMessageInbox(masterSecret, message,
contentLocation, threadId);
}

View File

@ -24,8 +24,17 @@ import android.util.Pair;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import java.io.File;
import java.io.IOException;
import java.util.List;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.GenericPdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduHeaders;
@ -39,6 +48,54 @@ public class MmsReceiver {
this.context = context;
}
public void process(MasterSecret masterSecret, Intent intent) {
try {
if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) {
handleMmsNotification(intent);
} else if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_MMS_ACTION)) {
handlePushMedia(masterSecret, intent);
}
} catch (MmsException e) {
Log.w("MmsReceiver", e);
}
}
private void handleMmsNotification(Intent intent) {
byte[] mmsData = intent.getByteArrayExtra("data");
PduParser parser = new PduParser(mmsData);
GenericPdu pdu = parser.parse();
if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu);
Log.w("MmsReceiver", "Inserted received MMS notification...");
scheduleDownload((NotificationInd)pdu, messageAndThreadId.first, messageAndThreadId.second);
}
}
private void handlePushMedia(MasterSecret masterSecret, Intent intent) throws MmsException {
IncomingPushMessage pushMessage = intent.getParcelableExtra("media_message");
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
try {
List<File> attachments = socket.retrieveAttachments(pushMessage.getAttachments());
IncomingMediaMessage message = new IncomingMediaMessage(localNumber, pushMessage, attachments);
DatabaseFactory.getMmsDatabase(context).insertMessageInbox(masterSecret, message, "", -1);
} catch (IOException e) {
Log.w("MmsReceiver", e);
try {
IncomingMediaMessage message = new IncomingMediaMessage(localNumber, pushMessage, null);
DatabaseFactory.getMmsDatabase(context).insertMessageInbox(masterSecret, message, "", -1);
} catch (IOException e1) {
throw new MmsException(e1);
}
}
}
private void scheduleDownload(NotificationInd pdu, long messageId, long threadId) {
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
intent.putExtra("content_location", new String(pdu.getContentLocation()));
@ -50,21 +107,4 @@ public class MmsReceiver {
context.startService(intent);
}
public void process(MasterSecret masterSecret, Intent intent) {
byte[] mmsData = intent.getByteArrayExtra("data");
PduParser parser = new PduParser(mmsData);
GenericPdu pdu = parser.parse();
if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu);
// long threadId = database.getThreadIdForMessage(messageId);
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
scheduleDownload((NotificationInd)pdu, messageAndThreadId.first, messageAndThreadId.second);
Log.w("MmsReceiverService", "Inserted received notification...");
}
}
}

View File

@ -5,24 +5,23 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Pair;
import com.google.android.gcm.GCMRegistrar;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.gcm.GcmIntentService;
import org.thoughtcrime.securesms.gcm.GcmRegistrationTimeoutException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.directory.DirectoryDescriptor;
import org.whispersystems.textsecure.directory.NumberFilter;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -172,7 +171,8 @@ public class RegistrationService extends Service {
String gcmRegistrationId = waitForGcmRegistrationId();
socket.registerGcmId(gcmRegistrationId);
socket.retrieveDirectory(this);
Pair<DirectoryDescriptor, File> directory = socket.retrieveDirectory();
NumberFilter.getInstance(this).update(directory.first, directory.second);
markAsVerified(number, password);
@ -190,10 +190,6 @@ public class RegistrationService extends Service {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_GCM_TIMEOUT));
broadcastComplete(false);
} catch (RateLimitException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR));
broadcastComplete(false);
} finally {
shutdownGcmRegistrationListener();
}
@ -222,7 +218,8 @@ public class RegistrationService extends Service {
String gcmRegistrationId = waitForGcmRegistrationId();
socket.registerGcmId(gcmRegistrationId);
socket.retrieveDirectory(this);
Pair<DirectoryDescriptor, File> directory = socket.retrieveDirectory();
NumberFilter.getInstance(this).update(directory.first, directory.second);
markAsVerified(number, password);
@ -244,10 +241,6 @@ public class RegistrationService extends Service {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_GCM_TIMEOUT));
broadcastComplete(false);
} catch (RateLimitException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR));
broadcastComplete(false);
} finally {
shutdownChallengeListener();
shutdownGcmRegistrationListener();

View File

@ -51,6 +51,7 @@ public class SendReceiveService extends Service {
public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION";
public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION";
public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION";
public static final String RECEIVE_PUSH_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_MMS_ACTION";
public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION";
public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION";
public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION";
@ -92,19 +93,21 @@ public class SendReceiveService extends Service {
public void onStart(Intent intent, int startId) {
if (intent == null) return;
if (intent.getAction().equals(SEND_SMS_ACTION))
String action = intent.getAction();
if (action.equals(SEND_SMS_ACTION))
scheduleSecretRequiredIntent(SEND_SMS, intent);
else if (intent.getAction().equals(RECEIVE_SMS_ACTION))
else if (action.equals(RECEIVE_SMS_ACTION))
scheduleIntent(RECEIVE_SMS, intent);
else if (intent.getAction().equals(SENT_SMS_ACTION))
else if (action.equals(SENT_SMS_ACTION))
scheduleIntent(SEND_SMS, intent);
else if (intent.getAction().equals(DELIVERED_SMS_ACTION))
else if (action.equals(DELIVERED_SMS_ACTION))
scheduleIntent(SEND_SMS, intent);
else if (intent.getAction().equals(SEND_MMS_ACTION))
else if (action.equals(SEND_MMS_ACTION))
scheduleSecretRequiredIntent(SEND_MMS, intent);
else if (intent.getAction().equals(RECEIVE_MMS_ACTION))
else if (action.equals(RECEIVE_MMS_ACTION) || action.equals(RECEIVE_PUSH_MMS_ACTION))
scheduleIntent(RECEIVE_MMS, intent);
else if (intent.getAction().equals(DOWNLOAD_MMS_ACTION))
else if (action.equals(DOWNLOAD_MMS_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent);

View File

@ -4,7 +4,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.telephony.SmsMessage;
import org.whispersystems.textsecure.push.IncomingGcmMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import java.util.List;
@ -40,7 +40,7 @@ public class IncomingTextMessage implements Parcelable {
this.sentTimestampMillis = message.getTimestampMillis();
}
public IncomingTextMessage(IncomingGcmMessage message) {
public IncomingTextMessage(IncomingPushMessage message) {
this.message = message.getMessageText();
this.sender = message.getSource();
this.protocol = 31337;

View File

@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.push.PushAttachment;
import org.whispersystems.textsecure.push.PushAttachmentData;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
@ -52,11 +52,11 @@ public class PushTransport extends BaseTransport {
public void deliver(SendReq message, List<String> destinations) throws IOException {
try {
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
String messageText = PartParser.getMessageText(message.getBody());
List<PushAttachment> attachments = getAttachmentsFromBody(message.getBody());
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
String messageText = PartParser.getMessageText(message.getBody());
List<PushAttachmentData> attachments = getAttachmentsFromBody(message.getBody());
if (attachments.isEmpty()) socket.sendMessage(destinations, messageText);
else socket.sendMessage(destinations, messageText, attachments);
@ -66,8 +66,8 @@ public class PushTransport extends BaseTransport {
}
}
private List<PushAttachment> getAttachmentsFromBody(PduBody body) {
List<PushAttachment> attachments = new LinkedList<PushAttachment>();
private List<PushAttachmentData> getAttachmentsFromBody(PduBody body) {
List<PushAttachmentData> attachments = new LinkedList<PushAttachmentData>();
for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType());
@ -76,7 +76,7 @@ public class PushTransport extends BaseTransport {
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
attachments.add(new PushAttachment(contentType, body.getPart(i).getData()));
attachments.add(new PushAttachmentData(contentType, body.getPart(i).getData()));
}
}

View File

@ -29,7 +29,11 @@ import android.provider.Telephony;
import org.thoughtcrime.securesms.mms.MmsRadio;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@ -151,6 +155,19 @@ public class Util {
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4069];
int read;
while ((read = in.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
in.close();
return baos.toByteArray();
}
public static boolean isDefaultSmsProvider(Context context){
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) ||