From a6e1d56cde4a05a1430011c1f01bab1cea27a298 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 21 Feb 2014 17:51:25 -0800 Subject: [PATCH] Refactor group messaging protocol. // FREEBIE --- .../protobuf/IncomingPushMessageSignal.proto | 8 +- .../textsecure/push/PushMessageProtos.java | 40 ++--- .../securesms/GroupCreateActivity.java | 145 ++++++++-------- .../securesms/database/GroupDatabase.java | 9 +- .../securesms/database/MmsDatabase.java | 3 +- .../securesms/database/MmsSmsColumns.java | 13 +- .../securesms/database/SmsDatabase.java | 3 +- .../database/model/DisplayRecord.java | 10 +- .../database/model/MessageRecord.java | 6 +- .../database/model/ThreadRecord.java | 6 +- .../mms/OutgoingGroupMediaMessage.java | 10 +- .../recipients/RecipientProvider.java | 22 +-- .../securesms/service/AvatarDownloader.java | 12 +- .../securesms/service/GroupReceiver.java | 159 ++++++++++++++++++ .../securesms/service/PushReceiver.java | 63 +------ .../securesms/sms/IncomingGroupMessage.java | 11 +- .../securesms/transport/PushTransport.java | 3 +- .../securesms/util/GroupUtil.java | 45 +++-- 18 files changed, 314 insertions(+), 254 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/service/GroupReceiver.java diff --git a/library/protobuf/IncomingPushMessageSignal.proto b/library/protobuf/IncomingPushMessageSignal.proto index 0ae98cd31f..bbd91167df 100644 --- a/library/protobuf/IncomingPushMessageSignal.proto +++ b/library/protobuf/IncomingPushMessageSignal.proto @@ -30,11 +30,9 @@ message PushMessageContent { message GroupContext { enum Type { UNKNOWN = 0; - CREATE = 1; - MODIFY = 2; - DELIVER = 3; - ADD = 4; - QUIT = 5; + UPDATE = 1; + DELIVER = 2; + QUIT = 3; } optional bytes id = 1; optional Type type = 2; diff --git a/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java b/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java index 018e0f7480..9bf618be35 100644 --- a/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java +++ b/library/src/org/whispersystems/textsecure/push/PushMessageProtos.java @@ -1463,19 +1463,15 @@ public final class PushMessageProtos { public enum Type implements com.google.protobuf.ProtocolMessageEnum { UNKNOWN(0, 0), - CREATE(1, 1), - MODIFY(2, 2), - DELIVER(3, 3), - ADD(4, 4), - QUIT(5, 5), + UPDATE(1, 1), + DELIVER(2, 2), + QUIT(3, 3), ; public static final int UNKNOWN_VALUE = 0; - public static final int CREATE_VALUE = 1; - public static final int MODIFY_VALUE = 2; - public static final int DELIVER_VALUE = 3; - public static final int ADD_VALUE = 4; - public static final int QUIT_VALUE = 5; + public static final int UPDATE_VALUE = 1; + public static final int DELIVER_VALUE = 2; + public static final int QUIT_VALUE = 3; public final int getNumber() { return value; } @@ -1483,11 +1479,9 @@ public final class PushMessageProtos { public static Type valueOf(int value) { switch (value) { case 0: return UNKNOWN; - case 1: return CREATE; - case 2: return MODIFY; - case 3: return DELIVER; - case 4: return ADD; - case 5: return QUIT; + case 1: return UPDATE; + case 2: return DELIVER; + case 3: return QUIT; default: return null; } } @@ -1518,7 +1512,7 @@ public final class PushMessageProtos { } private static final Type[] VALUES = { - UNKNOWN, CREATE, MODIFY, DELIVER, ADD, QUIT, + UNKNOWN, UPDATE, DELIVER, QUIT, }; public static Type valueOf( @@ -3073,22 +3067,22 @@ public final class PushMessageProtos { "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" + "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\"\234\004\n\022Push" + + "\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\210\002\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022>\n\004type\030\002" + + "\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\"K\n\004Type\022" + - "\013\n\007UNKNOWN\020\000\022\n\n\006CREATE\020\001\022\n\n\006MODIFY\020\002\022\013\n\007", - "DELIVER\020\003\022\007\n\003ADD\020\004\022\010\n\004QUIT\020\005\"\030\n\005Flags\022\017\n" + - "\013END_SESSION\020\001B7\n\"org.whispersystems.tex" + - "tsecure.pushB\021PushMessageProtos" + "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" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index f753c84516..6263a7e7a0 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -337,6 +337,11 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv } } + private void handleGroupUpdate() { + Log.w("GroupCreateActivity", "Creating..."); + new UpdateWhisperGroupAsyncTask().execute(); + } + private static List recipientsToNormalizedStrings(Collection recipients, String localNumber) { final List e164numbers = new ArrayList(recipients.size()); for (Recipient contact : recipients) { @@ -349,63 +354,6 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv return e164numbers; } - private void handleGroupUpdate() { - Log.i(TAG, "Updating group info."); - GroupDatabase db = DatabaseFactory.getGroupDatabase(this); - final String localNumber = TextSecurePreferences.getLocalNumber(this); - List e164numbers = recipientsToNormalizedStrings(selectedContacts, localNumber); - if (selectedContacts.size() > 0) { - db.add(groupId, localNumber, e164numbers); - GroupContext context = GroupContext.newBuilder() - .setId(ByteString.copyFrom(groupId)) - .setType(GroupContext.Type.ADD) - .addAllMembers(e164numbers) - .build(); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, null); - try { - MessageSender.send(this, masterSecret, outgoingMessage, groupThread); - } catch (MmsException me) { - Log.w(TAG, "MmsException encountered when trying to add members to group.", me); - } - } - - GroupContext.Builder builder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(groupId)) - .setType(GroupContext.Type.MODIFY); - boolean shouldSendUpdate = false; - final String title = groupName.getText().toString(); - if (existingTitle == null || (groupName.getText() != null && !existingTitle.equals(title))) { - builder.setName(title); - db.updateTitle(groupId, title); - shouldSendUpdate = true; - } - byte[] avatarBytes = null; - if (existingAvatarBmp == null || !existingAvatarBmp.equals(avatarBmp)) { - if (avatarBmp != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - avatarBmp.compress(Bitmap.CompressFormat.PNG, 100, stream); - avatarBytes = stream.toByteArray(); - } - db.updateAvatar(groupId, avatarBytes); - shouldSendUpdate = true; - } - - if (shouldSendUpdate) { - GroupContext context = builder.build(); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatarBytes); - try { - MessageSender.send(this, masterSecret, outgoingMessage, groupThread); - } catch (MmsException me) { - Log.w(TAG, "MmsException encountered when trying to add members to group.", me); - } - } - - RecipientFactory.clearCache(groupRecipient.getPrimaryRecipient()); - - setResult(RESULT_OK, getIntent()); - finish(); - } - private void enableWhisperGroupCreatingUi() { findViewById(R.id.group_details_layout).setVisibility(View.GONE); findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE); @@ -475,29 +423,56 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv } } - private Pair handleCreatePushGroup(String groupName, - byte[] avatar, + private Pair handleCreatePushGroup(String groupName, byte[] avatar, Set members) throws InvalidNumberException, MmsException { - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); - byte[] groupId = groupDatabase.allocateGroupId(); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); + byte[] groupId = groupDatabase.allocateGroupId(); + List memberE164Numbers = getE164Numbers(members); + + groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName, + memberE164Numbers, null, null); + groupDatabase.updateAvatar(groupId, avatar); + + return handlePushOperation(groupId, groupName, avatar, memberE164Numbers); + } + + private Pair handleUpdatePushGroup(byte[] groupId, String groupName, + byte[] avatar, Set members) + throws InvalidNumberException, MmsException + { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); + List memberE164Numbers = getE164Numbers(members); + + GroupDatabase.GroupRecord record = groupDatabase.getGroup(groupId); + Set newMembers = new HashSet(memberE164Numbers); + newMembers.removeAll(record.getMembers()); + + groupDatabase.add(groupId, TextSecurePreferences.getLocalNumber(this), + new LinkedList(newMembers)); + + groupDatabase.updateTitle(groupId, groupName); + groupDatabase.updateAvatar(groupId, avatar); + + + return handlePushOperation(groupId, groupName, avatar, memberE164Numbers); + } + + private Pair handlePushOperation(byte[] groupId, String groupName, byte[] avatar, + List e164numbers) + throws MmsException, InvalidNumberException + { try { - List memberE164Numbers = getE164Numbers(members); - String groupRecipientId = GroupUtil.getEncodedId(groupId); - - groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName, - memberE164Numbers, null, null); - groupDatabase.updateAvatar(groupId, avatar); - - Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false); + String groupRecipientId = GroupUtil.getEncodedId(groupId); + Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false); GroupContext context = GroupContext.newBuilder() .setId(ByteString.copyFrom(groupId)) - .setType(GroupContext.Type.CREATE) + .setType(GroupContext.Type.UPDATE) .setName(groupName) - .addAllMembers(memberE164Numbers) + .addAllMembers(e164numbers) .build(); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar); @@ -508,7 +483,6 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv throw new AssertionError(e); } catch (MmsException e) { Log.w(TAG, e); - groupDatabase.remove(groupId, TextSecurePreferences.getLocalNumber(this)); throw new MmsException(e); } } @@ -588,6 +562,30 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv } } + private class UpdateWhisperGroupAsyncTask extends AsyncTask> { + private long RES_BAD_NUMBER = -2; + private long RES_MMS_EXCEPTION = -3; + @Override + protected Pair doInBackground(Void... params) { + byte[] avatarBytes = null; + if (avatarBmp != null) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + avatarBmp.compress(Bitmap.CompressFormat.PNG, 100, stream); + avatarBytes = stream.toByteArray(); + } + final String name = (groupName.getText() != null) ? groupName.getText().toString() : null; + try { + return handleUpdatePushGroup(groupId, name, avatarBytes, selectedContacts); + } catch (MmsException e) { + Log.w(TAG, e); + return new Pair(RES_MMS_EXCEPTION, null); + } catch (InvalidNumberException e) { + Log.w(TAG, e); + return new Pair(RES_BAD_NUMBER, null); + } + } + } + private class CreateWhisperGroupAsyncTask extends AsyncTask> { private long RES_BAD_NUMBER = -2; private long RES_MMS_EXCEPTION = -3; @@ -664,8 +662,7 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv existingContacts.addAll(recipientList); } } - final GroupDatabase.Reader groupReader = db.getGroup(groupId); - GroupDatabase.GroupRecord group = groupReader.getNext(); + GroupDatabase.GroupRecord group = db.getGroup(groupId); if (group != null) { existingTitle = group.getTitle(); final byte[] existingAvatar = group.getAvatar(); diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index a30f8effc6..5dcd78b7e0 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -66,12 +66,16 @@ public class GroupDatabase extends Database { super(context, databaseHelper); } - public Reader getGroup(byte[] groupId) { + public GroupRecord getGroup(byte[] groupId) { Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(groupId)}, null, null, null); - return new Reader(cursor); + Reader reader = new Reader(cursor); + GroupRecord record = reader.getNext(); + + reader.close(); + return record; } public Recipients getGroupMembers(byte[] groupId) { @@ -107,7 +111,6 @@ public class GroupDatabase extends Database { } } - ContentValues contentValues = new ContentValues(); contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId)); contentValues.put(OWNER, owner); diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index bfb6b2d6a2..7a7359c3c9 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -532,9 +532,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns { } if (message.isGroup()) { - if (((OutgoingGroupMediaMessage)message).isGroupAdd()) type |= Types.GROUP_ADD_MEMBERS_BIT; + if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT; else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT; - else if (((OutgoingGroupMediaMessage)message).isGroupModify()) type |= Types.GROUP_MODIFY_BIT; } SendReq sendRequest = new SendReq(); diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 4e5511479a..17a0e20002 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -41,9 +41,8 @@ public interface MmsSmsColumns { protected static final long PUSH_MESSAGE_BIT = 0x200000; // Group Message Information - protected static final long GROUP_ADD_MEMBERS_BIT = 0x10000; - protected static final long GROUP_QUIT_BIT = 0x20000; - protected static final long GROUP_MODIFY_BIT = 0x40000; + protected static final long GROUP_UPDATE_BIT = 0x10000; + protected static final long GROUP_QUIT_BIT = 0x20000; // Encrypted Storage Information protected static final long ENCRYPTION_MASK = 0xFF000000; @@ -116,12 +115,8 @@ public interface MmsSmsColumns { return (type & KEY_EXCHANGE_IDENTITY_UPDATE_BIT) != 0; } - public static boolean isGroupAdd(long type) { - return (type & GROUP_ADD_MEMBERS_BIT) != 0; - } - - public static boolean isGroupModify(long type) { - return (type & GROUP_MODIFY_BIT) != 0; + public static boolean isGroupUpdate(long type) { + return (type & GROUP_UPDATE_BIT) != 0; } public static boolean isGroupQuit(long type) { diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index f749cae90c..338606b1c8 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -262,9 +262,8 @@ public class SmsDatabase extends Database implements MmsSmsColumns { type |= Types.ENCRYPTION_REMOTE_BIT; } else if (message.isGroup()) { type |= Types.SECURE_MESSAGE_BIT; - if (((IncomingGroupMessage)message).isAdd()) type |= Types.GROUP_ADD_MEMBERS_BIT; + if (((IncomingGroupMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT; else if (((IncomingGroupMessage)message).isQuit()) type |= Types.GROUP_QUIT_BIT; - else if (((IncomingGroupMessage)message).isModify()) type |= Types.GROUP_MODIFY_BIT; } else if (message.isEndSession()) { type |= Types.SECURE_MESSAGE_BIT; type |= Types.END_SESSION_BIT; diff --git a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 2ed0e51364..3c0bc3ed44 100644 --- a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -83,12 +83,8 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isEndSessionType(type); } - public boolean isGroupAdd() { - return SmsDatabase.Types.isGroupAdd(type); - } - - public boolean isGroupModify() { - return SmsDatabase.Types.isGroupModify(type); + public boolean isGroupUpdate() { + return SmsDatabase.Types.isGroupUpdate(type); } public boolean isGroupQuit() { @@ -96,7 +92,7 @@ public abstract class DisplayRecord { } public boolean isGroupAction() { - return isGroupAdd() || isGroupModify() || isGroupQuit(); + return isGroupUpdate() || isGroupQuit(); } public static class Body { diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 41ac7a1a2b..a196af93a8 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -84,12 +84,10 @@ public abstract class MessageRecord extends DisplayRecord { @Override public SpannableString getDisplayBody() { - if (isGroupAdd()) { - return emphasisAdded(context.getString(R.string.ConversationItem_group_action_joined, Util.join(GroupUtil.getSerializedArgumentMembers(getBody().getBody()), ", "))); + if (isGroupUpdate()) { + return emphasisAdded(GroupUtil.getDescription(getBody().getBody())); } else if (isGroupQuit()) { return emphasisAdded(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString())); - } else if (isGroupModify()) { - return emphasisAdded(context.getString(R.string.ConversationItem_group_action_modify, getIndividualRecipient().toShortString())); } return new SpannableString(getBody().getBody()); diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 42ccedc522..a87b0403d5 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -56,12 +56,10 @@ public class ThreadRecord extends DisplayRecord { // 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 (isGroupAdd()) { - return emphasisAdded(Util.join(GroupUtil.getSerializedArgumentMembers(getBody().getBody()), ", ") + " have joined the group"); + } else if (isGroupUpdate()) { + return emphasisAdded(GroupUtil.getDescription(getBody().getBody())); } else if (isGroupQuit()) { return emphasisAdded(getRecipients().toShortString() + " left the group."); - } else if (isGroupModify()) { - return emphasisAdded(getRecipients().toShortString() + " modified the group."); } else if (isKeyExchange()) { return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message)); } else if (SmsDatabase.Types.isFailedDecryptType(type)) { diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index e20b5e3813..57435fd5b8 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -37,17 +37,11 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { return true; } - public boolean isGroupAdd() { - return - group.getType().getNumber() == GroupContext.Type.ADD_VALUE || - group.getType().getNumber() == GroupContext.Type.CREATE_VALUE; + public boolean isGroupUpdate() { + return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE; } public boolean isGroupQuit() { return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE; } - - public boolean isGroupModify() { - return group.getType().getNumber() == GroupContext.Type.MODIFY_VALUE; - } } diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index 24b7f19f2a..893214245d 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -144,23 +144,17 @@ public class RecipientProvider { private RecipientDetails getGroupRecipientDetails(Context context, String groupId) { try { - GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context) - .getGroup(GroupUtil.getDecodedId(groupId)); + GroupDatabase.GroupRecord record = DatabaseFactory.getGroupDatabase(context) + .getGroup(GroupUtil.getDecodedId(groupId)); - GroupDatabase.GroupRecord record; + if (record != null) { + byte[] avatarBytes = record.getAvatar(); + Bitmap avatar; - try { - if ((record = reader.getNext()) != null) { - byte[] avatarBytes = record.getAvatar(); - Bitmap avatar; + if (avatarBytes == null) avatar = ContactPhotoFactory.getDefaultContactPhoto(context); + else avatar = BitmapFactory.decodeByteArray(avatarBytes, 0, avatarBytes.length); - if (avatarBytes == null) avatar = ContactPhotoFactory.getDefaultContactPhoto(context); - else avatar = BitmapFactory.decodeByteArray(avatarBytes, 0, avatarBytes.length); - - return new RecipientDetails(record.getTitle(), null, avatar); - } - } finally { - reader.close(); + return new RecipientDetails(record.getTitle(), null, avatar); } return null; diff --git a/src/org/thoughtcrime/securesms/service/AvatarDownloader.java b/src/org/thoughtcrime/securesms/service/AvatarDownloader.java index 8eedd3c7d7..82dd46b0e4 100644 --- a/src/org/thoughtcrime/securesms/service/AvatarDownloader.java +++ b/src/org/thoughtcrime/securesms/service/AvatarDownloader.java @@ -37,19 +37,17 @@ public class AvatarDownloader { if (!SendReceiveService.DOWNLOAD_AVATAR_ACTION.equals(intent.getAction())) return; - byte[] groupId = intent.getByteArrayExtra("group_id"); - GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - GroupDatabase.Reader reader = database.getGroup(groupId); + byte[] groupId = intent.getByteArrayExtra("group_id"); + GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + GroupDatabase.GroupRecord record = database.getGroup(groupId); - GroupDatabase.GroupRecord record; - - while ((record = reader.getNext()) != null) { + if (record != null) { long avatarId = record.getAvatarId(); byte[] key = record.getAvatarKey(); String relay = record.getRelay(); if (avatarId == -1 || key == null) { - continue; + return; } File attachment = downloadAttachment(relay, avatarId); diff --git a/src/org/thoughtcrime/securesms/service/GroupReceiver.java b/src/org/thoughtcrime/securesms/service/GroupReceiver.java new file mode 100644 index 0000000000..fe35090f32 --- /dev/null +++ b/src/org/thoughtcrime/securesms/service/GroupReceiver.java @@ -0,0 +1,159 @@ +package org.thoughtcrime.securesms.service; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.util.Pair; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.sms.IncomingGroupMessage; +import org.thoughtcrime.securesms.sms.IncomingTextMessage; +import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.push.IncomingPushMessage; +import org.whispersystems.textsecure.util.Base64; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; +import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; +import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; + +public class GroupReceiver { + + private final Context context; + + public GroupReceiver(Context context) { + this.context = context.getApplicationContext(); + } + + public void process(MasterSecret masterSecret, + IncomingPushMessage message, + PushMessageContent messageContent, + boolean secure) + { + if (!messageContent.getGroup().hasId()) { + Log.w("GroupReceiver", "Received group message with no id! Ignoring..."); + return; + } + + if (!secure) { + Log.w("GroupReceiver", "Received insecure group push action! Ignoring..."); + return; + } + + GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + GroupContext group = messageContent.getGroup(); + byte[] id = group.getId().toByteArray(); + int type = group.getType().getNumber(); + GroupRecord record = database.getGroup(id); + + if (record != null && type == GroupContext.Type.UPDATE_VALUE) { + handleGroupUpdate(masterSecret, message, group, record); + } else if (record == null && type == GroupContext.Type.UPDATE_VALUE) { + handleGroupCreate(masterSecret, message, group); + } else if (type == GroupContext.Type.QUIT_VALUE) { + handleGroupLeave(masterSecret, message, group, record); + } else if (type == GroupContext.Type.UNKNOWN_VALUE) { + Log.w("GroupReceiver", "Received unknown type, ignoring..."); + } + } + + private void handleGroupCreate(MasterSecret masterSecret, + IncomingPushMessage message, + GroupContext group) + { + GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + byte[] id = group.getId().toByteArray(); + + database.create(id, message.getSource(), group.getName(), group.getMembersList(), + group.getAvatar(), message.getRelay()); + + storeMessage(masterSecret, message, group); + } + + private void handleGroupUpdate(MasterSecret masterSecret, + IncomingPushMessage message, + GroupContext group, + GroupRecord groupRecord) + { + + GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + byte[] id = group.getId().toByteArray(); + + Set recordMembers = new HashSet(groupRecord.getMembers()); + Set messageMembers = new HashSet(group.getMembersList()); + + Set addedMembers = new HashSet(messageMembers); + addedMembers.removeAll(recordMembers); + + Set missingMembers = new HashSet(recordMembers); + missingMembers.removeAll(messageMembers); + + if (addedMembers.size() > 0) { + Set unionMembers = new HashSet(recordMembers); + unionMembers.addAll(messageMembers); + database.add(id, message.getSource(), new LinkedList(unionMembers)); + + group = group.toBuilder().clearMembers().addAllMembers(addedMembers).build(); + } else { + group = group.toBuilder().clearMembers().build(); + } + + if (missingMembers.size() > 0) { + // TODO We should tell added and missing about each-other. + } + + if (group.hasName() || group.hasAvatar()) { + database.update(id, message.getSource(), group.getName(), group.getAvatar()); + } + + if (group.hasName() && group.getName() != null && group.getName().equals(groupRecord.getTitle())) { + group = group.toBuilder().clearName().build(); + } + + storeMessage(masterSecret, message, group); + } + + private void handleGroupLeave(MasterSecret masterSecret, + IncomingPushMessage message, + GroupContext group, + GroupRecord record) + { + GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + byte[] id = group.getId().toByteArray(); + List members = record.getMembers(); + + if (members.contains(message.getSource())) { + database.remove(id, message.getSource()); + + storeMessage(masterSecret, message, group); + } + } + + + private void storeMessage(MasterSecret masterSecret, IncomingPushMessage message, GroupContext group) { + if (group.hasAvatar()) { + Intent intent = new Intent(context, SendReceiveService.class); + intent.setAction(SendReceiveService.DOWNLOAD_AVATAR_ACTION); + intent.putExtra("group_id", group.getId().toByteArray()); + context.startService(intent); + } + + EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context); + String body = Base64.encodeBytes(group.toByteArray()); + IncomingTextMessage incoming = new IncomingTextMessage(message, body, group); + IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, group, body); + + Pair messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage); + smsDatabase.updateMessageBody(masterSecret, messageAndThreadId.first, body); + + MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); + } + +} diff --git a/src/org/thoughtcrime/securesms/service/PushReceiver.java b/src/org/thoughtcrime/securesms/service/PushReceiver.java index deab385b9d..70191acd0c 100644 --- a/src/org/thoughtcrime/securesms/service/PushReceiver.java +++ b/src/org/thoughtcrime/securesms/service/PushReceiver.java @@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; -import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.notifications.MessageNotifier; @@ -22,7 +21,6 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; -import org.thoughtcrime.securesms.sms.IncomingGroupMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; @@ -38,11 +36,9 @@ import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.Session; -import org.whispersystems.textsecure.util.Base64; import ws.com.google.android.mms.MmsException; -import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext.Type; public class PushReceiver { @@ -51,10 +47,12 @@ public class PushReceiver { public static final int RESULT_NO_SESSION = 1; public static final int RESULT_DECRYPT_FAILED = 2; - private final Context context; + private final Context context; + private final GroupReceiver groupReceiver; public PushReceiver(Context context) { - this.context = context.getApplicationContext(); + this.context = context.getApplicationContext(); + this.groupReceiver = new GroupReceiver(context); } public void process(MasterSecret masterSecret, Intent intent) { @@ -160,7 +158,7 @@ public class PushReceiver { handleEndSessionMessage(masterSecret, message, messageContent); } else if (messageContent.hasGroup() && messageContent.getGroup().getType().getNumber() != Type.DELIVER_VALUE) { Log.w("PushReceiver", "Received push group message..."); - handleReceivedGroupMessage(masterSecret, message, messageContent, secure); + groupReceiver.process(masterSecret, message, messageContent, secure); } else if (messageContent.getAttachmentsCount() > 0) { Log.w("PushReceiver", "Received push media message..."); handleReceivedMediaMessage(masterSecret, message, messageContent, secure); @@ -174,57 +172,6 @@ public class PushReceiver { } } - private void handleReceivedGroupMessage(MasterSecret masterSecret, - IncomingPushMessage message, - PushMessageContent messageContent, - boolean secure) - { - if (!messageContent.getGroup().hasId()) { - Log.w("PushReceiver", "Received group message with no id!"); - return; - } - - if (!secure) { - Log.w("PushReceiver", "Received insecure group push action!"); - return; - } - - GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - GroupContext group = messageContent.getGroup(); - byte[] id = group.getId().toByteArray(); - int type = group.getType().getNumber(); - - if (type == Type.CREATE_VALUE) { - database.create(id, message.getSource(), group.getName(), group.getMembersList(), group.getAvatar(), message.getRelay()); - } else if (type == Type.ADD_VALUE) { - database.add(id, message.getSource(), group.getMembersList()); - } else if (type == Type.QUIT_VALUE) { - database.remove(id, message.getSource()); - } else if (type == Type.MODIFY_VALUE) { - database.update(id, message.getSource(), group.getName(), group.getAvatar()); - } else if (type == Type.UNKNOWN_VALUE) { - Log.w("PushReceiver", "Receied group message from unknown type: " + type); - return; - } - - if (group.hasAvatar()) { - Intent intent = new Intent(context, SendReceiveService.class); - intent.setAction(SendReceiveService.DOWNLOAD_AVATAR_ACTION); - intent.putExtra("group_id", group.getId().toByteArray()); - context.startService(intent); - } - - EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context); - String body = Base64.encodeBytes(group.toByteArray()); - IncomingTextMessage incoming = new IncomingTextMessage(message, body, group); - IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, group, body); - - Pair messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage); - smsDatabase.updateMessageBody(masterSecret, messageAndThreadId.first, body); - - MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); - } - private void handleEndSessionMessage(MasterSecret masterSecret, IncomingPushMessage message, PushMessageContent messageContent) diff --git a/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java index 376c494b3f..2c282d8df0 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.sms; import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.util.GroupUtil; -import org.whispersystems.textsecure.push.PushMessageProtos; import java.io.IOException; @@ -28,20 +27,14 @@ public class IncomingGroupMessage extends IncomingTextMessage { return true; } - public boolean isAdd() { - return - groupContext.getType().getNumber() == GroupContext.Type.ADD_VALUE || - groupContext.getType().getNumber() == GroupContext.Type.CREATE_VALUE; + public boolean isUpdate() { + return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE; } public boolean isQuit() { return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE; } - public boolean isModify() { - return groupContext.getType().getNumber() == GroupContext.Type.MODIFY_VALUE; - } - public static IncomingGroupMessage createForQuit(String groupId, String user) throws IOException { IncomingTextMessage base = new IncomingTextMessage(user, groupId); GroupContext context = GroupContext.newBuilder() diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index c77203dfa5..414ddcc2c7 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -250,8 +250,7 @@ public class PushTransport extends BaseTransport { groupBuilder.setId(ByteString.copyFrom(groupId)); groupBuilder.setType(GroupContext.Type.DELIVER); - if (MmsSmsColumns.Types.isGroupAdd(message.getDatabaseMessageBox()) || - MmsSmsColumns.Types.isGroupModify(message.getDatabaseMessageBox()) || + if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) || MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox())) { if (messageBody != null && messageBody.trim().length() > 0) { diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 2258483b20..1809193506 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util; import android.util.Log; import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Hex; @@ -33,34 +34,32 @@ public class GroupUtil { return groupId.startsWith(ENCODED_GROUP_PREFIX); } - public static boolean isMetaGroupAction(int groupAction) { - return groupAction > 0 - && groupAction != GroupContext.Type.DELIVER_VALUE; - } - - 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(); + public static String getDescription(String encodedGroup) { + if (encodedGroup == null) { + return "Group updated."; } try { - GroupContext context = GroupContext.parseFrom(Base64.decode(serialized)); - return context.getMembersList(); + String description = ""; + GroupContext context = GroupContext.parseFrom(Base64.decode(encodedGroup)); + List members = context.getMembersList(); + String title = context.getName(); + + if (!members.isEmpty()) { + description += org.whispersystems.textsecure.util.Util.join(members, ", ") + " joined the group."; + } + + if (title != null && !title.trim().isEmpty()) { + description += " Updated title to '" + title + "'."; + } + + return description; + } catch (InvalidProtocolBufferException e) { + Log.w("GroupUtil", e); + return "Group updated."; } catch (IOException e) { Log.w("GroupUtil", e); - return new LinkedList(); + return "Group updated."; } } }