diff --git a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java index 2eeec0ba3d..6b416155cc 100644 --- a/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java +++ b/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java @@ -5,7 +5,6 @@ import android.util.Log; import com.google.thoughtcrimegson.Gson; import com.google.thoughtcrimegson.JsonParseException; -import com.google.thoughtcrimegson.JsonSyntaxException; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.whispersystems.textsecure.crypto.IdentityKey; @@ -27,7 +26,6 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -94,7 +92,7 @@ public class PushServiceSocket { try { makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle)); } catch (NotFoundException nfe) { - throw new UnregisteredUserException(nfe); + throw new UnregisteredUserException(bundle.getDestination(), nfe); } } diff --git a/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java b/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java index ae87b39006..9b3131527e 100644 --- a/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java +++ b/library/src/org/whispersystems/textsecure/push/UnregisteredUserException.java @@ -5,8 +5,14 @@ import java.util.List; public class UnregisteredUserException extends IOException { - public UnregisteredUserException(Exception exception) { + private final String e164number; + + public UnregisteredUserException(String e164number, Exception exception) { super(exception); + this.e164number = e164number; } + public String getE164Number() { + return e164number; + } } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index b74aab090c..b679b13ab5 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -34,6 +34,7 @@ import android.provider.ContactsContract.QuickContact; import org.thoughtcrime.securesms.util.DateUtils; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.View; import android.widget.Button; import android.widget.ImageView; @@ -42,6 +43,7 @@ import android.widget.TextView; import android.widget.Toast; import android.webkit.MimeTypeMap; +import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.textsecure.crypto.MasterSecret; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -53,14 +55,17 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Emoji; +import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.FutureTaskListener; import org.whispersystems.textsecure.util.ListenableFutureTask; +import org.whispersystems.textsecure.util.Util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; @@ -172,13 +177,14 @@ public class ConversationItem extends LinearLayout { /// MessageRecord Attribute Parsers private void setBodyText(MessageRecord messageRecord) { + // TODO jake is going to fill these in switch (messageRecord.getGroupAction()) { case GroupContext.Type.QUIT_VALUE: bodyText.setText(messageRecord.getIndividualRecipient().toShortString() + " has left the group."); return; case GroupContext.Type.ADD_VALUE: case GroupContext.Type.CREATE_VALUE: - bodyText.setText(messageRecord.getGroupActionArguments() + " have joined the group."); + bodyText.setText(Util.join(GroupUtil.getSerializedArgumentMembers(messageRecord.getGroupActionArguments()), ", ") + " have joined the group."); return; case GroupContext.Type.MODIFY_VALUE: bodyText.setText(messageRecord.getIndividualRecipient() + " has updated the group."); diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index 17bd94804b..47ce656461 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -9,7 +9,6 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; -import android.util.Pair; import android.view.View; import android.widget.EditText; import android.widget.ImageView; @@ -30,8 +29,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; -import org.thoughtcrime.securesms.transport.PushTransport; +import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.ActionBarUtil; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -42,7 +40,7 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.NotInDirectoryException; -import org.whispersystems.textsecure.push.PushAttachmentPointer; +import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.InvalidNumberException; import java.io.ByteArrayOutputStream; @@ -54,8 +52,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import ws.com.google.android.mms.MmsException; + import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; -import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; @@ -355,59 +354,32 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv } } - private Pair> handleCreatePushGroup(String groupName, - byte[] avatar, - Set members) + private long handleCreatePushGroup(String groupName, + byte[] avatar, + Set members) throws IOException, InvalidNumberException { - List memberE164Numbers = getE164Numbers(members); - PushTransport transport = new PushTransport(this, masterSecret); - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this); - byte[] groupId = groupDatabase.allocateGroupId(); - AttachmentPointer avatarPointer = null; - - memberE164Numbers.add(TextSecurePreferences.getLocalNumber(this)); - - GroupContext.Builder builder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(groupId)) - .setType(GroupContext.Type.CREATE) - .setName(groupName) - .addAllMembers(memberE164Numbers); - - if (avatar != null) { - PushAttachmentPointer pointer = transport.createAttachment("image/png", avatar); - avatarPointer = AttachmentPointer.newBuilder() - .setKey(ByteString.copyFrom(pointer.getKey())) - .setContentType(pointer.getContentType()) - .setId(pointer.getId()).build(); - builder.setAvatar(avatarPointer); - } - - List failures = transport.deliver(new LinkedList(members), builder.build()); - groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName, - memberE164Numbers, avatarPointer, null); - - if (avatar != null) { - groupDatabase.updateAvatar(groupId, avatar); - } - try { - String groupRecipientId = GroupUtil.getEncodedId(groupId); - Recipient groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false).getPrimaryRecipient(); - OutgoingTextMessage outgoing = new OutgoingTextMessage(groupRecipient, GroupContext.Type.ADD_VALUE, org.whispersystems.textsecure.util.Util.join(memberE164Numbers, ",")); - long threadId = threadDatabase.getThreadIdFor(new Recipients(groupRecipient)); - List messageIds = DatabaseFactory.getEncryptingSmsDatabase(this) - .insertMessageOutbox(masterSecret, threadId, outgoing); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); + List memberE164Numbers = getE164Numbers(members); + byte[] groupId = groupDatabase.allocateGroupId(); + String groupRecipientId = GroupUtil.getEncodedId(groupId); - for (long messageId : messageIds) { - DatabaseFactory.getEncryptingSmsDatabase(this).markAsSent(messageId); - } + String groupActionArguments = GroupUtil.serializeArguments(groupId, groupName, memberE164Numbers); + groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName, + memberE164Numbers, null, null); + groupDatabase.updateAvatar(groupId, avatar); - return new Pair>(threadId, failures); + Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false); + + return MessageSender.sendGroupAction(this, masterSecret, groupRecipient, -1, + GroupContext.Type.CREATE_VALUE, + groupActionArguments, avatar); } catch (RecipientFormattingException e) { - throw new AssertionError(e); + throw new IOException(e); + } catch (MmsException e) { + throw new IOException(e); } } diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index b69d8ce32b..87f50a2653 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -132,7 +132,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns { CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION, 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 + DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID, + GROUP_ACTION, GROUP_ACTION_ARGUMENTS }; public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor(); @@ -343,10 +344,13 @@ public class MmsDatabase extends Database implements MmsSmsColumns { int i = 0; while (cursor.moveToNext()) { - messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); - String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); - PduHeaders headers = getHeadersFromCursor(cursor); + messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); + + long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); + String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); + int groupAction = cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_ACTION)); + String groupActionArguments = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ACTION_ARGUMENTS)); + PduHeaders headers = getHeadersFromCursor(cursor); addr.getAddressesForId(messageId, headers); PduBody body = getPartsAsBody(partDatabase.getParts(messageId, true)); @@ -361,7 +365,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, groupAction, groupActionArguments); } return requests; @@ -514,6 +518,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns { contentValues.put(THREAD_ID, threadId); contentValues.put(READ, 1); contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT)); + contentValues.put(GROUP_ACTION, sendRequest.getGroupAction()); + contentValues.put(GROUP_ACTION_ARGUMENTS, sendRequest.getGroupActionArguments()); contentValues.remove(ADDRESS); long messageId = insertMediaMessage(masterSecret, sendRequest.getPduHeaders(), diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index f6ef86bea4..53deaa760b 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -20,13 +20,17 @@ import android.content.Context; import android.text.Spannable; import android.text.SpannableString; import android.text.style.StyleSpan; +import android.util.Pair; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.textsecure.push.PushMessageProtos; import org.whispersystems.textsecure.util.Util; +import java.util.List; + import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; /** @@ -55,12 +59,13 @@ public class ThreadRecord extends DisplayRecord { @Override public SpannableString getDisplayBody() { + // TODO jake is going to fill these in if (SmsDatabase.Types.isDecryptInProgressType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait)); } else if (getGroupAction() == GroupContext.Type.ADD_VALUE || getGroupAction() == GroupContext.Type.CREATE_VALUE) { - return emphasisAdded("Added " + getGroupActionArguments()); + return emphasisAdded(Util.join(GroupUtil.getSerializedArgumentMembers(getGroupActionArguments()), ", ") + " have joined the group"); } else if (getGroupAction() == GroupContext.Type.QUIT_VALUE) { return emphasisAdded(getRecipients().toShortString() + " left the group."); } else if (getGroupAction() == GroupContext.Type.MODIFY_VALUE) { diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index f1f458bb9b..8afded126f 100644 --- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -41,7 +41,7 @@ public class IncomingMediaMessage { if (messageContent.hasGroup()) { this.groupId = GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray()); this.groupAction = messageContent.getGroup().getType().getNumber(); - this.groupActionArguments = GroupUtil.getActionArgument(messageContent.getGroup()); + this.groupActionArguments = GroupUtil.serializeArguments(messageContent.getGroup()); } else { this.groupId = null; this.groupAction = -1; diff --git a/src/org/thoughtcrime/securesms/service/MmsSender.java b/src/org/thoughtcrime/securesms/service/MmsSender.java index 58ff6a1c02..4863e49e90 100644 --- a/src/org/thoughtcrime/securesms/service/MmsSender.java +++ b/src/org/thoughtcrime/securesms/service/MmsSender.java @@ -92,8 +92,7 @@ public class MmsSender { Recipients recipients = threads.getRecipientsForThreadId(threadId); MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); } catch (UntrustedIdentityException uie) { - IncomingTextMessage base = new IncomingTextMessage(message); - IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(uie.getIdentityKey().serialize())); + IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey()); DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage); database.markAsSentFailed(messageId); } catch (RetryLaterException e) { diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java index a08cde0a3c..f8ddce0e7a 100644 --- a/src/org/thoughtcrime/securesms/service/SmsSender.java +++ b/src/org/thoughtcrime/securesms/service/SmsSender.java @@ -81,8 +81,7 @@ public class SmsSender { transport.deliver(record); } catch (UntrustedIdentityException e) { Log.w("SmsSender", e); - IncomingTextMessage base = new IncomingTextMessage(record); - IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(e.getIdentityKey().serialize())); + IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey()); DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); } catch (UndeliverableMessageException ude) { diff --git a/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java index e494e691e5..bec307b224 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java @@ -1,5 +1,8 @@ package org.thoughtcrime.securesms.sms; +import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.util.Base64; + public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage { public IncomingIdentityUpdateMessage(IncomingTextMessage base, String newBody) { @@ -15,4 +18,13 @@ public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage { public boolean isIdentityUpdate() { return true; } + + public static IncomingIdentityUpdateMessage createFor(String sender, IdentityKey identityKey) { + return createFor(sender, identityKey, null); + } + + public static IncomingIdentityUpdateMessage createFor(String sender, IdentityKey identityKey, String groupId) { + IncomingTextMessage base = new IncomingTextMessage(sender, groupId, -1, null); + return new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(identityKey.serialize())); + } } diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 71e3ebd861..80079dfb95 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -68,7 +68,7 @@ public class IncomingTextMessage implements Parcelable { if (group != null) { this.groupId = GroupUtil.getEncodedId(group.getId().toByteArray()); this.groupAction = group.getType().getNumber(); - this.groupActionArgument = GroupUtil.getActionArgument(group); + this.groupActionArgument = GroupUtil.serializeArguments(group); } else { this.groupId = null; this.groupAction = -1; @@ -152,6 +152,22 @@ public class IncomingTextMessage implements Parcelable { this.groupActionArgument = null; } + protected IncomingTextMessage(String sender, String groupId, + int groupAction, String groupActionArgument) + { + this.message = ""; + this.sender = sender; + this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID; + this.protocol = 31338; + this.serviceCenterAddress = "Outgoing"; + this.replyPathPresent = true; + this.pseudoSubject = ""; + this.sentTimestampMillis = System.currentTimeMillis(); + this.groupId = groupId; + this.groupAction = groupAction; + this.groupActionArgument = groupActionArgument; + } + public long getSentTimestampMillis() { return sentTimestampMillis; } @@ -235,4 +251,8 @@ public class IncomingTextMessage implements Parcelable { out.writeInt(groupAction); out.writeString(groupActionArgument); } + + public static IncomingTextMessage createForLeavingGroup(String groupId, String user) { + return new IncomingTextMessage(user, groupId, GroupContext.Type.QUIT_VALUE, null); + } } diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index bd7b156e5b..9e1c834b98 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.util.Log; +import org.thoughtcrime.securesms.mms.ImageSlide; import org.whispersystems.textsecure.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -34,10 +35,43 @@ import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.PduBody; +import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.SendReq; public class MessageSender { + public static long sendGroupAction(Context context, MasterSecret masterSecret, Recipients recipients, + long threadId, int groupAction, String groupActionArguments, byte[] avatar) + throws MmsException + { + if (threadId == -1) { + threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); + } + + PduBody body = new PduBody(); + + if (avatar != null) { + PduPart part = new PduPart(); + part.setData(avatar); + part.setContentType(ContentType.IMAGE_PNG.getBytes()); + part.setContentId((System.currentTimeMillis()+"").getBytes()); + part.setName(("Image" + System.currentTimeMillis()).getBytes()); + body.addPart(part); + } + + SendReq sendRequest = new SendReq(); + sendRequest.setDate(System.currentTimeMillis() / 1000L); + sendRequest.setBody(body); + sendRequest.setContentType(ContentType.MULTIPART_MIXED.getBytes()); + sendRequest.setGroupAction(groupAction); + sendRequest.setGroupActionArguments(groupActionArguments); + + sendMms(context, recipients, masterSecret, sendRequest, threadId, + ThreadDatabase.DistributionTypes.CONVERSATION, true); + + return threadId; + } + public static long sendMms(Context context, MasterSecret masterSecret, Recipients recipients, long threadId, SlideDeck slideDeck, String message, int distributionType, boolean secure) diff --git a/src/org/thoughtcrime/securesms/transport/EncapsulatedExceptions.java b/src/org/thoughtcrime/securesms/transport/EncapsulatedExceptions.java new file mode 100644 index 0000000000..5122ef4a03 --- /dev/null +++ b/src/org/thoughtcrime/securesms/transport/EncapsulatedExceptions.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.transport; + +import org.whispersystems.textsecure.push.UnregisteredUserException; + +import java.util.List; + +public class EncapsulatedExceptions extends Throwable { + + private final List untrustedIdentityExceptions; + private final List unregisteredUserExceptions; + + public EncapsulatedExceptions(List untrustedIdentities, + List unregisteredUsers) + { + this.untrustedIdentityExceptions = untrustedIdentities; + this.unregisteredUserExceptions = unregisteredUsers; + } + + public List getUntrustedIdentityExceptions() { + return untrustedIdentityExceptions; + } + + public List getUnregisteredUserExceptions() { + return unregisteredUserExceptions; + } +} diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index 4c2406450a..8312d07330 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -51,6 +51,7 @@ import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.UnregisteredUserException; import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.InvalidNumberException; import java.io.IOException; @@ -62,6 +63,8 @@ import ws.com.google.android.mms.pdu.PduBody; import ws.com.google.android.mms.pdu.SendReq; import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal; +import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer; +import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; public class PushTransport extends BaseTransport { @@ -100,7 +103,7 @@ public class PushTransport extends BaseTransport { } public void deliver(SendReq message, long threadId) - throws IOException, UntrustedIdentityException + throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions { PushServiceSocket socket = PushServiceSocketFactory.create(context); byte[] plaintext = getPlaintextMessage(socket, message); @@ -108,67 +111,31 @@ public class PushTransport extends BaseTransport { Recipients recipients; - try { - if (GroupUtil.isEncodedGroup(destination)) { - recipients = DatabaseFactory.getGroupDatabase(context) - .getGroupMembers(GroupUtil.getDecodedId(destination)); - } else { - recipients = RecipientFactory.getRecipientsFromString(context, destination, false); - } - - for (Recipient recipient : recipients.getRecipientsList()) { - deliver(socket, recipient, threadId, plaintext); - } - } catch (UnregisteredUserException uue) { - // TODO: We should probably remove the user from the directory? - throw new IOException(uue); - } catch (RecipientFormattingException e) { - throw new IOException(e); - } catch (InvalidNumberException e) { - throw new IOException(e); + if (GroupUtil.isEncodedGroup(destination)) { + recipients = DatabaseFactory.getGroupDatabase(context) + .getGroupMembers(GroupUtil.getDecodedId(destination)); + } else { + recipients = RecipientFactory.getRecipientsFromString(context, destination, false); } - } - public List deliver(List recipients, - PushMessageContent.GroupContext groupAction) - throws IOException - { - PushServiceSocket socket = PushServiceSocketFactory.create(context); - byte[] plaintext = PushMessageContent.newBuilder() - .setGroup(groupAction) - .build().toByteArray(); - List failures = new LinkedList(); + List untrustedIdentities = new LinkedList(); + List unregisteredUsers = new LinkedList(); - for (Recipient recipient : recipients) { + for (Recipient recipient : recipients.getRecipientsList()) { try { - deliver(socket, recipient, -1, plaintext); - } catch (UnregisteredUserException e) { - Log.w("PushTransport", e); - failures.add(recipient); - } catch (InvalidNumberException e) { - Log.w("PushTransport", e); - failures.add(recipient); - } catch (IOException e) { - Log.w("PushTransport", e); - failures.add(recipient); + deliver(socket, recipient, threadId, plaintext); } catch (UntrustedIdentityException e) { Log.w("PushTransport", e); - failures.add(recipient); + untrustedIdentities.add(e); + } catch (UnregisteredUserException e) { + Log.w("PushTransport", e); + unregisteredUsers.add(e); } } - if (failures.size() == recipients.size()) { - throw new IOException("Total failure."); + if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) { + throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers); } - - return failures; - } - - public PushAttachmentPointer createAttachment(String contentType, byte[] data) - throws IOException - { - PushServiceSocket socket = PushServiceSocketFactory.create(context); - return getPushAttachmentPointer(socket, contentType, data); } private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext) @@ -252,11 +219,40 @@ public class PushTransport extends BaseTransport { PushMessageContent.Builder builder = PushMessageContent.newBuilder(); if (GroupUtil.isEncodedGroup(message.getTo()[0].getString())) { - PushMessageContent.GroupContext.Builder groupBuilder = - PushMessageContent.GroupContext.newBuilder(); + byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString()); + GroupContext.Builder groupBuilder = GroupContext.newBuilder(); - groupBuilder.setType(PushMessageContent.GroupContext.Type.DELIVER); - groupBuilder.setId(ByteString.copyFrom(GroupUtil.getDecodedId(message.getTo()[0].getString()))); + groupBuilder.setId(ByteString.copyFrom(groupId)); + + switch (message.getGroupAction()) { + case GroupContext.Type.ADD_VALUE: groupBuilder.setType(GroupContext.Type.ADD); break; + case GroupContext.Type.CREATE_VALUE: groupBuilder.setType(GroupContext.Type.CREATE); break; + case GroupContext.Type.QUIT_VALUE: groupBuilder.setType(GroupContext.Type.QUIT); break; + default: groupBuilder.setType(GroupContext.Type.DELIVER); break; + } + + if (message.getGroupAction() == GroupContext.Type.ADD_VALUE || + message.getGroupAction() == GroupContext.Type.CREATE_VALUE) + { + GroupContext serialized = GroupContext.parseFrom(Base64.decode(message.getGroupActionArguments())); + groupBuilder.addAllMembers(serialized.getMembersList()); + + if (serialized.hasName()) { + groupBuilder.setName(serialized.getName()); + } + } + + if (message.getGroupAction() == GroupContext.Type.CREATE_VALUE && !attachments.isEmpty()) { + Log.w("PushTransport", "Adding avatar..."); + groupBuilder.setAvatar(AttachmentPointer.newBuilder() + .setId(attachments.get(0).getId()) + .setContentType(attachments.get(0).getContentType()) + .setKey(ByteString.copyFrom(attachments.get(0).getKey())) + .build()); + attachments.remove(0); + } else { + Log.w("PushTransport", "Not adding avatar: " + message.getGroupAction() + " , " + attachments.isEmpty()); + } builder.setGroup(groupBuilder.build()); } @@ -266,8 +262,8 @@ public class PushTransport extends BaseTransport { } for (PushAttachmentPointer attachment : attachments) { - PushMessageContent.AttachmentPointer.Builder attachmentBuilder = - PushMessageContent.AttachmentPointer.newBuilder(); + AttachmentPointer.Builder attachmentBuilder = + AttachmentPointer.newBuilder(); attachmentBuilder.setId(attachment.getId()); attachmentBuilder.setContentType(attachment.getContentType()); @@ -316,7 +312,7 @@ public class PushTransport extends BaseTransport { if (processor.isTrusted(preKey)) { processor.processKeyExchangeMessage(preKey, threadId); } else { - throw new UntrustedIdentityException("Untrusted identity key!", preKey.getIdentityKey()); + throw new UntrustedIdentityException("Untrusted identity key!", pushAddress.getNumber(), preKey.getIdentityKey()); } } } catch (InvalidKeyException e) { diff --git a/src/org/thoughtcrime/securesms/transport/RetryLaterException.java b/src/org/thoughtcrime/securesms/transport/RetryLaterException.java index c8bf75e5ba..0642f7c691 100644 --- a/src/org/thoughtcrime/securesms/transport/RetryLaterException.java +++ b/src/org/thoughtcrime/securesms/transport/RetryLaterException.java @@ -1,4 +1,9 @@ package org.thoughtcrime.securesms.transport; +import java.io.IOException; + public class RetryLaterException extends Exception { + public RetryLaterException(Exception e) { + super(e); + } } diff --git a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java index d948c77416..1c725e00e0 100644 --- a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java +++ b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java @@ -19,10 +19,14 @@ package org.thoughtcrime.securesms.transport; import android.content.Context; import android.util.Log; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.push.PushServiceSocketFactory; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFormattingException; +import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage; +import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -31,7 +35,11 @@ import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.push.ContactNumberDetails; import org.whispersystems.textsecure.push.ContactTokenDetails; +import org.whispersystems.textsecure.push.IncomingPushMessage; +import org.whispersystems.textsecure.push.PushMessageProtos; import org.whispersystems.textsecure.push.PushServiceSocket; +import org.whispersystems.textsecure.push.UnregisteredUserException; +import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.util.DirectoryUtil; import org.whispersystems.textsecure.util.InvalidNumberException; @@ -39,15 +47,19 @@ import java.io.IOException; import ws.com.google.android.mms.pdu.SendReq; +import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal; + public class UniversalTransport { private final Context context; + private final MasterSecret masterSecret; private final PushTransport pushTransport; private final SmsTransport smsTransport; private final MmsTransport mmsTransport; public UniversalTransport(Context context, MasterSecret masterSecret) { this.context = context; + this.masterSecret = masterSecret; this.pushTransport = new PushTransport(context, masterSecret); this.smsTransport = new SmsTransport(context, masterSecret); this.mmsTransport = new MmsTransport(context, masterSecret); @@ -90,6 +102,10 @@ public class UniversalTransport { throw new UndeliverableMessageException("No destination specified"); } + if (GroupUtil.isEncodedGroup(mediaMessage.getTo()[0].getString())) { + return deliverGroupMessage(mediaMessage, threadId); + } + if (!TextSecurePreferences.isPushRegistered(context)) { return mmsTransport.deliver(mediaMessage); } @@ -98,34 +114,77 @@ public class UniversalTransport { return mmsTransport.deliver(mediaMessage); } - String destination; - try { - destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString()); + String destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString()); + + if (isPushTransport(destination)) { + try { + Log.w("UniversalTransport", "Delivering media message with GCM..."); + pushTransport.deliver(mediaMessage, threadId); + return new MmsSendResult("push".getBytes("UTF-8"), 0, true); + } catch (IOException ioe) { + Log.w("UniversalTransport", ioe); + return mmsTransport.deliver(mediaMessage); + } catch (RecipientFormattingException e) { + Log.w("UniversalTransport", e); + return mmsTransport.deliver(mediaMessage); + } catch (EncapsulatedExceptions ee) { + Log.w("UniversalTransport", ee); + if (!ee.getUnregisteredUserExceptions().isEmpty()) { + return mmsTransport.deliver(mediaMessage); + } else { + throw new UntrustedIdentityException(ee.getUntrustedIdentityExceptions().get(0)); + } + } + } else { + Log.w("UniversalTransport", "Delivering media message with MMS..."); + return mmsTransport.deliver(mediaMessage); + } } catch (InvalidNumberException ine) { Log.w("UniversalTransport", ine); return mmsTransport.deliver(mediaMessage); } + } - if (isPushTransport(destination)) { + private MmsSendResult deliverGroupMessage(SendReq mediaMessage, long threadId) + throws RetryLaterException, UndeliverableMessageException + { + if (!TextSecurePreferences.isPushRegistered(context)) { + throw new UndeliverableMessageException("Not push registered!"); + } + + try { + pushTransport.deliver(mediaMessage, threadId); + return new MmsSendResult("push".getBytes("UTF-8"), 0, true); + } catch (IOException e) { + Log.w("UniversalTransport", e); + throw new RetryLaterException(e); + } catch (RecipientFormattingException e) { + throw new UndeliverableMessageException(e); + } catch (InvalidNumberException e) { + throw new UndeliverableMessageException(e); + } catch (EncapsulatedExceptions ee) { + Log.w("UniversalTransport", ee); try { - Log.w("UniversalTransport", "Delivering media message with GCM..."); - pushTransport.deliver(mediaMessage, threadId); + for (UnregisteredUserException unregistered : ee.getUnregisteredUserExceptions()) { + IncomingTextMessage quitMessage = IncomingTextMessage.createForLeavingGroup(mediaMessage.getTo()[0].getString(), unregistered.getE164Number()); + DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, quitMessage); + DatabaseFactory.getGroupDatabase(context).remove(GroupUtil.getDecodedId(mediaMessage.getTo()[0].getString()), unregistered.getE164Number()); + } + + for (UntrustedIdentityException untrusted : ee.getUntrustedIdentityExceptions()) { + IncomingIdentityUpdateMessage identityMessage = IncomingIdentityUpdateMessage.createFor(untrusted.getE164Number(), untrusted.getIdentityKey(), mediaMessage.getTo()[0].getString()); + DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityMessage); + } + return new MmsSendResult("push".getBytes("UTF-8"), 0, true); } catch (IOException ioe) { - Log.w("UniversalTransport", ioe); - if (!GroupUtil.isEncodedGroup(destination)) { - return mmsTransport.deliver(mediaMessage); - } else { - throw new RetryLaterException(); - } + throw new AssertionError(ioe); } - } else { - Log.w("UniversalTransport", "Delivering media message with MMS..."); - return mmsTransport.deliver(mediaMessage); } } + public boolean isMultipleRecipients(SendReq mediaMessage) { int recipientCount = 0; diff --git a/src/org/thoughtcrime/securesms/transport/UntrustedIdentityException.java b/src/org/thoughtcrime/securesms/transport/UntrustedIdentityException.java index e14f79332f..ac322f3921 100644 --- a/src/org/thoughtcrime/securesms/transport/UntrustedIdentityException.java +++ b/src/org/thoughtcrime/securesms/transport/UntrustedIdentityException.java @@ -1,17 +1,29 @@ package org.thoughtcrime.securesms.transport; import org.whispersystems.textsecure.crypto.IdentityKey; +import org.whispersystems.textsecure.push.UnregisteredUserException; public class UntrustedIdentityException extends Exception { private final IdentityKey identityKey; + private final String e164number; - public UntrustedIdentityException(String s, IdentityKey identityKey) { + public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) { super(s); + this.e164number = e164number; this.identityKey = identityKey; } + public UntrustedIdentityException(UntrustedIdentityException e) { + this(e.getMessage(), e.getE164Number(), e.getIdentityKey()); + } + public IdentityKey getIdentityKey() { return identityKey; } + + public String getE164Number() { + return e164number; + } + } diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 89a76cd4a4..98ffa44ba4 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -1,8 +1,16 @@ package org.thoughtcrime.securesms.util; +import android.util.Log; +import android.util.Pair; + +import com.google.protobuf.ByteString; + +import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Hex; import java.io.IOException; +import java.util.LinkedList; +import java.util.List; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; @@ -26,15 +34,29 @@ public class GroupUtil { return groupId.startsWith(ENCODED_GROUP_PREFIX); } - public static String getActionArgument(GroupContext group) { - if (group.getType().equals(GroupContext.Type.CREATE) || - group.getType().equals(GroupContext.Type.ADD)) - { - return org.whispersystems.textsecure.util.Util.join(group.getMembersList(), ","); - } else if (group.getType().equals(GroupContext.Type.MODIFY)) { - return group.getName(); + public static String serializeArguments(byte[] id, String name, List members) { + return Base64.encodeBytes(GroupContext.newBuilder() + .setId(ByteString.copyFrom(id)) + .setName(name) + .addAllMembers(members) + .build().toByteArray()); + } + + public static String serializeArguments(GroupContext context) { + return Base64.encodeBytes(context.toByteArray()); + } + + public static List getSerializedArgumentMembers(String serialized) { + if (serialized == null) { + return new LinkedList(); } - return null; + try { + GroupContext context = GroupContext.parseFrom(Base64.decode(serialized)); + return context.getMembersList(); + } catch (IOException e) { + Log.w("GroupUtil", e); + return new LinkedList(); + } } } diff --git a/src/ws/com/google/android/mms/pdu/SendReq.java b/src/ws/com/google/android/mms/pdu/SendReq.java index 19f0bb0d57..23022e1ccf 100644 --- a/src/ws/com/google/android/mms/pdu/SendReq.java +++ b/src/ws/com/google/android/mms/pdu/SendReq.java @@ -25,6 +25,8 @@ public class SendReq extends MultimediaMessagePdu { private static final String TAG = "SendReq"; private long databaseMessageId; private long messageBox; + private int groupAction; + private String groupActionArguments; public SendReq() { super(); @@ -90,11 +92,15 @@ 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, + int groupAction, String groupActionArguments) + { super(headers, body); - this.databaseMessageId = messageId; - this.messageBox = messageBox; - } + this.databaseMessageId = messageId; + this.messageBox = messageBox; + this.groupAction = groupAction; + this.groupActionArguments = groupActionArguments; + } public long getDatabaseMessageBox() { return this.messageBox; @@ -103,6 +109,22 @@ public class SendReq extends MultimediaMessagePdu { public long getDatabaseMessageId() { return databaseMessageId; } + + public int getGroupAction() { + return this.groupAction; + } + + public String getGroupActionArguments() { + return this.groupActionArguments; + } + + public void setGroupAction(int groupAction) { + this.groupAction = groupAction; + } + + public void setGroupActionArguments(String groupActionArguments) { + this.groupActionArguments = groupActionArguments; + } /** * Get Bcc value.