Refactor group messaging protocol.

// FREEBIE
pull/1/head
Moxie Marlinspike 11 years ago
parent b855f8a163
commit a6e1d56cde

@ -30,11 +30,9 @@ message PushMessageContent {
message GroupContext { message GroupContext {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0;
CREATE = 1; UPDATE = 1;
MODIFY = 2; DELIVER = 2;
DELIVER = 3; QUIT = 3;
ADD = 4;
QUIT = 5;
} }
optional bytes id = 1; optional bytes id = 1;
optional Type type = 2; optional Type type = 2;

@ -1463,19 +1463,15 @@ public final class PushMessageProtos {
public enum Type public enum Type
implements com.google.protobuf.ProtocolMessageEnum { implements com.google.protobuf.ProtocolMessageEnum {
UNKNOWN(0, 0), UNKNOWN(0, 0),
CREATE(1, 1), UPDATE(1, 1),
MODIFY(2, 2), DELIVER(2, 2),
DELIVER(3, 3), QUIT(3, 3),
ADD(4, 4),
QUIT(5, 5),
; ;
public static final int UNKNOWN_VALUE = 0; public static final int UNKNOWN_VALUE = 0;
public static final int CREATE_VALUE = 1; public static final int UPDATE_VALUE = 1;
public static final int MODIFY_VALUE = 2; public static final int DELIVER_VALUE = 2;
public static final int DELIVER_VALUE = 3; public static final int QUIT_VALUE = 3;
public static final int ADD_VALUE = 4;
public static final int QUIT_VALUE = 5;
public final int getNumber() { return value; } public final int getNumber() { return value; }
@ -1483,11 +1479,9 @@ public final class PushMessageProtos {
public static Type valueOf(int value) { public static Type valueOf(int value) {
switch (value) { switch (value) {
case 0: return UNKNOWN; case 0: return UNKNOWN;
case 1: return CREATE; case 1: return UPDATE;
case 2: return MODIFY; case 2: return DELIVER;
case 3: return DELIVER; case 3: return QUIT;
case 4: return ADD;
case 5: return QUIT;
default: return null; default: return null;
} }
} }
@ -1518,7 +1512,7 @@ public final class PushMessageProtos {
} }
private static final Type[] VALUES = { private static final Type[] VALUES = {
UNKNOWN, CREATE, MODIFY, DELIVER, ADD, QUIT, UNKNOWN, UPDATE, DELIVER, QUIT,
}; };
public static Type valueOf( 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" + "evice\030\007 \001(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030" +
"\005 \001(\004\022\017\n\007message\030\006 \001(\014\"W\n\004Type\022\013\n\007UNKNOW" + "\005 \001(\004\022\017\n\007message\030\006 \001(\014\"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" + "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" + "MessageContent\022\014\n\004body\030\001 \001(\t\022E\n\013attachme" +
"nts\030\002 \003(\01320.textsecure.PushMessageConten", "nts\030\002 \003(\01320.textsecure.PushMessageConten",
"t.AttachmentPointer\022:\n\005group\030\003 \001(\0132+.tex" + "t.AttachmentPointer\022:\n\005group\030\003 \001(\0132+.tex" +
"tsecure.PushMessageContent.GroupContext\022" + "tsecure.PushMessageContent.GroupContext\022" +
"\r\n\005flags\030\004 \001(\r\032A\n\021AttachmentPointer\022\n\n\002i" + "\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(" + "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" + " \001(\01620.textsecure.PushMessageContent.Gro" +
"upContext.Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030" + "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" + "\004 \003(\t\022@\n\006avatar\030\005 \001(\01320.textsecure.PushM" +
"essageContent.AttachmentPointer\"K\n\004Type\022" + "essageContent.AttachmentPointer\"6\n\004Type\022" +
"\013\n\007UNKNOWN\020\000\022\n\n\006CREATE\020\001\022\n\n\006MODIFY\020\002\022\013\n\007", "\013\n\007UNKNOWN\020\000\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n",
"DELIVER\020\003\022\007\n\003ADD\020\004\022\010\n\004QUIT\020\005\"\030\n\005Flags\022\017\n" + "\004QUIT\020\003\"\030\n\005Flags\022\017\n\013END_SESSION\020\001B7\n\"org" +
"\013END_SESSION\020\001B7\n\"org.whispersystems.tex" + ".whispersystems.textsecure.pushB\021PushMes" +
"tsecure.pushB\021PushMessageProtos" "sageProtos"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {

@ -337,6 +337,11 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
} }
} }
private void handleGroupUpdate() {
Log.w("GroupCreateActivity", "Creating...");
new UpdateWhisperGroupAsyncTask().execute();
}
private static List<String> recipientsToNormalizedStrings(Collection<Recipient> recipients, String localNumber) { private static List<String> recipientsToNormalizedStrings(Collection<Recipient> recipients, String localNumber) {
final List<String> e164numbers = new ArrayList<String>(recipients.size()); final List<String> e164numbers = new ArrayList<String>(recipients.size());
for (Recipient contact : recipients) { for (Recipient contact : recipients) {
@ -349,63 +354,6 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
return e164numbers; return e164numbers;
} }
private void handleGroupUpdate() {
Log.i(TAG, "Updating group info.");
GroupDatabase db = DatabaseFactory.getGroupDatabase(this);
final String localNumber = TextSecurePreferences.getLocalNumber(this);
List<String> 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() { private void enableWhisperGroupCreatingUi() {
findViewById(R.id.group_details_layout).setVisibility(View.GONE); findViewById(R.id.group_details_layout).setVisibility(View.GONE);
findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE); findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE);
@ -475,29 +423,56 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
} }
} }
private Pair<Long, Recipients> handleCreatePushGroup(String groupName, private Pair<Long, Recipients> handleCreatePushGroup(String groupName, byte[] avatar,
byte[] avatar,
Set<Recipient> members) Set<Recipient> members)
throws InvalidNumberException, MmsException throws InvalidNumberException, MmsException
{ {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
byte[] groupId = groupDatabase.allocateGroupId(); byte[] groupId = groupDatabase.allocateGroupId();
try {
List<String> memberE164Numbers = getE164Numbers(members); List<String> memberE164Numbers = getE164Numbers(members);
String groupRecipientId = GroupUtil.getEncodedId(groupId);
groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName, groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName,
memberE164Numbers, null, null); memberE164Numbers, null, null);
groupDatabase.updateAvatar(groupId, avatar); groupDatabase.updateAvatar(groupId, avatar);
return handlePushOperation(groupId, groupName, avatar, memberE164Numbers);
}
private Pair<Long, Recipients> handleUpdatePushGroup(byte[] groupId, String groupName,
byte[] avatar, Set<Recipient> members)
throws InvalidNumberException, MmsException
{
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
List<String> memberE164Numbers = getE164Numbers(members);
GroupDatabase.GroupRecord record = groupDatabase.getGroup(groupId);
Set<String> newMembers = new HashSet<String>(memberE164Numbers);
newMembers.removeAll(record.getMembers());
groupDatabase.add(groupId, TextSecurePreferences.getLocalNumber(this),
new LinkedList<String>(newMembers));
groupDatabase.updateTitle(groupId, groupName);
groupDatabase.updateAvatar(groupId, avatar);
return handlePushOperation(groupId, groupName, avatar, memberE164Numbers);
}
private Pair<Long, Recipients> handlePushOperation(byte[] groupId, String groupName, byte[] avatar,
List<String> e164numbers)
throws MmsException, InvalidNumberException
{
try {
String groupRecipientId = GroupUtil.getEncodedId(groupId);
Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false); Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false);
GroupContext context = GroupContext.newBuilder() GroupContext context = GroupContext.newBuilder()
.setId(ByteString.copyFrom(groupId)) .setId(ByteString.copyFrom(groupId))
.setType(GroupContext.Type.CREATE) .setType(GroupContext.Type.UPDATE)
.setName(groupName) .setName(groupName)
.addAllMembers(memberE164Numbers) .addAllMembers(e164numbers)
.build(); .build();
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar);
@ -508,7 +483,6 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
throw new AssertionError(e); throw new AssertionError(e);
} catch (MmsException e) { } catch (MmsException e) {
Log.w(TAG, e); Log.w(TAG, e);
groupDatabase.remove(groupId, TextSecurePreferences.getLocalNumber(this));
throw new MmsException(e); throw new MmsException(e);
} }
} }
@ -588,6 +562,30 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
} }
} }
private class UpdateWhisperGroupAsyncTask extends AsyncTask<Void,Void,Pair<Long,Recipients>> {
private long RES_BAD_NUMBER = -2;
private long RES_MMS_EXCEPTION = -3;
@Override
protected Pair<Long, Recipients> 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<Long,Recipients>(RES_MMS_EXCEPTION, null);
} catch (InvalidNumberException e) {
Log.w(TAG, e);
return new Pair<Long,Recipients>(RES_BAD_NUMBER, null);
}
}
}
private class CreateWhisperGroupAsyncTask extends AsyncTask<Void,Void,Pair<Long,Recipients>> { private class CreateWhisperGroupAsyncTask extends AsyncTask<Void,Void,Pair<Long,Recipients>> {
private long RES_BAD_NUMBER = -2; private long RES_BAD_NUMBER = -2;
private long RES_MMS_EXCEPTION = -3; private long RES_MMS_EXCEPTION = -3;
@ -664,8 +662,7 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
existingContacts.addAll(recipientList); existingContacts.addAll(recipientList);
} }
} }
final GroupDatabase.Reader groupReader = db.getGroup(groupId); GroupDatabase.GroupRecord group = db.getGroup(groupId);
GroupDatabase.GroupRecord group = groupReader.getNext();
if (group != null) { if (group != null) {
existingTitle = group.getTitle(); existingTitle = group.getTitle();
final byte[] existingAvatar = group.getAvatar(); final byte[] existingAvatar = group.getAvatar();

@ -66,12 +66,16 @@ public class GroupDatabase extends Database {
super(context, databaseHelper); super(context, databaseHelper);
} }
public Reader getGroup(byte[] groupId) { public GroupRecord getGroup(byte[] groupId) {
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?", Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)}, new String[] {GroupUtil.getEncodedId(groupId)},
null, null, null); 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) { public Recipients getGroupMembers(byte[] groupId) {
@ -107,7 +111,6 @@ public class GroupDatabase extends Database {
} }
} }
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId)); contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId));
contentValues.put(OWNER, owner); contentValues.put(OWNER, owner);

@ -532,9 +532,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
} }
if (message.isGroup()) { 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).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
else if (((OutgoingGroupMediaMessage)message).isGroupModify()) type |= Types.GROUP_MODIFY_BIT;
} }
SendReq sendRequest = new SendReq(); SendReq sendRequest = new SendReq();

@ -41,9 +41,8 @@ public interface MmsSmsColumns {
protected static final long PUSH_MESSAGE_BIT = 0x200000; protected static final long PUSH_MESSAGE_BIT = 0x200000;
// Group Message Information // Group Message Information
protected static final long GROUP_ADD_MEMBERS_BIT = 0x10000; protected static final long GROUP_UPDATE_BIT = 0x10000;
protected static final long GROUP_QUIT_BIT = 0x20000; protected static final long GROUP_QUIT_BIT = 0x20000;
protected static final long GROUP_MODIFY_BIT = 0x40000;
// Encrypted Storage Information // Encrypted Storage Information
protected static final long ENCRYPTION_MASK = 0xFF000000; protected static final long ENCRYPTION_MASK = 0xFF000000;
@ -116,12 +115,8 @@ public interface MmsSmsColumns {
return (type & KEY_EXCHANGE_IDENTITY_UPDATE_BIT) != 0; return (type & KEY_EXCHANGE_IDENTITY_UPDATE_BIT) != 0;
} }
public static boolean isGroupAdd(long type) { public static boolean isGroupUpdate(long type) {
return (type & GROUP_ADD_MEMBERS_BIT) != 0; return (type & GROUP_UPDATE_BIT) != 0;
}
public static boolean isGroupModify(long type) {
return (type & GROUP_MODIFY_BIT) != 0;
} }
public static boolean isGroupQuit(long type) { public static boolean isGroupQuit(long type) {

@ -262,9 +262,8 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
type |= Types.ENCRYPTION_REMOTE_BIT; type |= Types.ENCRYPTION_REMOTE_BIT;
} else if (message.isGroup()) { } else if (message.isGroup()) {
type |= Types.SECURE_MESSAGE_BIT; 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).isQuit()) type |= Types.GROUP_QUIT_BIT;
else if (((IncomingGroupMessage)message).isModify()) type |= Types.GROUP_MODIFY_BIT;
} else if (message.isEndSession()) { } else if (message.isEndSession()) {
type |= Types.SECURE_MESSAGE_BIT; type |= Types.SECURE_MESSAGE_BIT;
type |= Types.END_SESSION_BIT; type |= Types.END_SESSION_BIT;

@ -83,12 +83,8 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isEndSessionType(type); return SmsDatabase.Types.isEndSessionType(type);
} }
public boolean isGroupAdd() { public boolean isGroupUpdate() {
return SmsDatabase.Types.isGroupAdd(type); return SmsDatabase.Types.isGroupUpdate(type);
}
public boolean isGroupModify() {
return SmsDatabase.Types.isGroupModify(type);
} }
public boolean isGroupQuit() { public boolean isGroupQuit() {
@ -96,7 +92,7 @@ public abstract class DisplayRecord {
} }
public boolean isGroupAction() { public boolean isGroupAction() {
return isGroupAdd() || isGroupModify() || isGroupQuit(); return isGroupUpdate() || isGroupQuit();
} }
public static class Body { public static class Body {

@ -84,12 +84,10 @@ public abstract class MessageRecord extends DisplayRecord {
@Override @Override
public SpannableString getDisplayBody() { public SpannableString getDisplayBody() {
if (isGroupAdd()) { if (isGroupUpdate()) {
return emphasisAdded(context.getString(R.string.ConversationItem_group_action_joined, Util.join(GroupUtil.getSerializedArgumentMembers(getBody().getBody()), ", "))); return emphasisAdded(GroupUtil.getDescription(getBody().getBody()));
} else if (isGroupQuit()) { } else if (isGroupQuit()) {
return emphasisAdded(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString())); 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()); return new SpannableString(getBody().getBody());

@ -56,12 +56,10 @@ public class ThreadRecord extends DisplayRecord {
// TODO jake is going to fill these in // TODO jake is going to fill these in
if (SmsDatabase.Types.isDecryptInProgressType(type)) { if (SmsDatabase.Types.isDecryptInProgressType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait)); return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait));
} else if (isGroupAdd()) { } else if (isGroupUpdate()) {
return emphasisAdded(Util.join(GroupUtil.getSerializedArgumentMembers(getBody().getBody()), ", ") + " have joined the group"); return emphasisAdded(GroupUtil.getDescription(getBody().getBody()));
} else if (isGroupQuit()) { } else if (isGroupQuit()) {
return emphasisAdded(getRecipients().toShortString() + " left the group."); return emphasisAdded(getRecipients().toShortString() + " left the group.");
} else if (isGroupModify()) {
return emphasisAdded(getRecipients().toShortString() + " modified the group.");
} else if (isKeyExchange()) { } else if (isKeyExchange()) {
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message)); return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
} else if (SmsDatabase.Types.isFailedDecryptType(type)) { } else if (SmsDatabase.Types.isFailedDecryptType(type)) {

@ -37,17 +37,11 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
return true; return true;
} }
public boolean isGroupAdd() { public boolean isGroupUpdate() {
return return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
group.getType().getNumber() == GroupContext.Type.ADD_VALUE ||
group.getType().getNumber() == GroupContext.Type.CREATE_VALUE;
} }
public boolean isGroupQuit() { public boolean isGroupQuit() {
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE; return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
} }
public boolean isGroupModify() {
return group.getType().getNumber() == GroupContext.Type.MODIFY_VALUE;
}
} }

@ -144,13 +144,10 @@ public class RecipientProvider {
private RecipientDetails getGroupRecipientDetails(Context context, String groupId) { private RecipientDetails getGroupRecipientDetails(Context context, String groupId) {
try { try {
GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context) GroupDatabase.GroupRecord record = DatabaseFactory.getGroupDatabase(context)
.getGroup(GroupUtil.getDecodedId(groupId)); .getGroup(GroupUtil.getDecodedId(groupId));
GroupDatabase.GroupRecord record; if (record != null) {
try {
if ((record = reader.getNext()) != null) {
byte[] avatarBytes = record.getAvatar(); byte[] avatarBytes = record.getAvatar();
Bitmap avatar; Bitmap avatar;
@ -159,9 +156,6 @@ public class RecipientProvider {
return new RecipientDetails(record.getTitle(), null, avatar); return new RecipientDetails(record.getTitle(), null, avatar);
} }
} finally {
reader.close();
}
return null; return null;
} catch (IOException e) { } catch (IOException e) {

@ -39,17 +39,15 @@ public class AvatarDownloader {
byte[] groupId = intent.getByteArrayExtra("group_id"); byte[] groupId = intent.getByteArrayExtra("group_id");
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.Reader reader = database.getGroup(groupId); GroupDatabase.GroupRecord record = database.getGroup(groupId);
GroupDatabase.GroupRecord record; if (record != null) {
while ((record = reader.getNext()) != null) {
long avatarId = record.getAvatarId(); long avatarId = record.getAvatarId();
byte[] key = record.getAvatarKey(); byte[] key = record.getAvatarKey();
String relay = record.getRelay(); String relay = record.getRelay();
if (avatarId == -1 || key == null) { if (avatarId == -1 || key == null) {
continue; return;
} }
File attachment = downloadAttachment(relay, avatarId); File attachment = downloadAttachment(relay, avatarId);

@ -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<String> recordMembers = new HashSet<String>(groupRecord.getMembers());
Set<String> messageMembers = new HashSet<String>(group.getMembersList());
Set<String> addedMembers = new HashSet<String>(messageMembers);
addedMembers.removeAll(recordMembers);
Set<String> missingMembers = new HashSet<String>(recordMembers);
missingMembers.removeAll(messageMembers);
if (addedMembers.size() > 0) {
Set<String> unionMembers = new HashSet<String>(recordMembers);
unionMembers.addAll(messageMembers);
database.add(id, message.getSource(), new LinkedList<String>(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<String> 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<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage);
smsDatabase.updateMessageBody(masterSecret, messageAndThreadId.first, body);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
}

@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier; 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.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; 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.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.Session; import org.whispersystems.textsecure.storage.Session;
import org.whispersystems.textsecure.util.Base64;
import ws.com.google.android.mms.MmsException; 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; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext.Type;
public class PushReceiver { public class PushReceiver {
@ -52,9 +48,11 @@ public class PushReceiver {
public static final int RESULT_DECRYPT_FAILED = 2; public static final int RESULT_DECRYPT_FAILED = 2;
private final Context context; private final Context context;
private final GroupReceiver groupReceiver;
public PushReceiver(Context context) { public PushReceiver(Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.groupReceiver = new GroupReceiver(context);
} }
public void process(MasterSecret masterSecret, Intent intent) { public void process(MasterSecret masterSecret, Intent intent) {
@ -160,7 +158,7 @@ public class PushReceiver {
handleEndSessionMessage(masterSecret, message, messageContent); handleEndSessionMessage(masterSecret, message, messageContent);
} else if (messageContent.hasGroup() && messageContent.getGroup().getType().getNumber() != Type.DELIVER_VALUE) { } else if (messageContent.hasGroup() && messageContent.getGroup().getType().getNumber() != Type.DELIVER_VALUE) {
Log.w("PushReceiver", "Received push group message..."); Log.w("PushReceiver", "Received push group message...");
handleReceivedGroupMessage(masterSecret, message, messageContent, secure); groupReceiver.process(masterSecret, message, messageContent, secure);
} else if (messageContent.getAttachmentsCount() > 0) { } else if (messageContent.getAttachmentsCount() > 0) {
Log.w("PushReceiver", "Received push media message..."); Log.w("PushReceiver", "Received push media message...");
handleReceivedMediaMessage(masterSecret, message, messageContent, secure); 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<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage);
smsDatabase.updateMessageBody(masterSecret, messageAndThreadId.first, body);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleEndSessionMessage(MasterSecret masterSecret, private void handleEndSessionMessage(MasterSecret masterSecret,
IncomingPushMessage message, IncomingPushMessage message,
PushMessageContent messageContent) PushMessageContent messageContent)

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.sms;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.push.PushMessageProtos;
import java.io.IOException; import java.io.IOException;
@ -28,20 +27,14 @@ public class IncomingGroupMessage extends IncomingTextMessage {
return true; return true;
} }
public boolean isAdd() { public boolean isUpdate() {
return return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
groupContext.getType().getNumber() == GroupContext.Type.ADD_VALUE ||
groupContext.getType().getNumber() == GroupContext.Type.CREATE_VALUE;
} }
public boolean isQuit() { public boolean isQuit() {
return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE; 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 { public static IncomingGroupMessage createForQuit(String groupId, String user) throws IOException {
IncomingTextMessage base = new IncomingTextMessage(user, groupId); IncomingTextMessage base = new IncomingTextMessage(user, groupId);
GroupContext context = GroupContext.newBuilder() GroupContext context = GroupContext.newBuilder()

@ -250,8 +250,7 @@ public class PushTransport extends BaseTransport {
groupBuilder.setId(ByteString.copyFrom(groupId)); groupBuilder.setId(ByteString.copyFrom(groupId));
groupBuilder.setType(GroupContext.Type.DELIVER); groupBuilder.setType(GroupContext.Type.DELIVER);
if (MmsSmsColumns.Types.isGroupAdd(message.getDatabaseMessageBox()) || if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupModify(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox())) MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
{ {
if (messageBody != null && messageBody.trim().length() > 0) { if (messageBody != null && messageBody.trim().length() > 0) {

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util;
import android.util.Log; import android.util.Log;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
@ -33,34 +34,32 @@ public class GroupUtil {
return groupId.startsWith(ENCODED_GROUP_PREFIX); return groupId.startsWith(ENCODED_GROUP_PREFIX);
} }
public static boolean isMetaGroupAction(int groupAction) { public static String getDescription(String encodedGroup) {
return groupAction > 0 if (encodedGroup == null) {
&& groupAction != GroupContext.Type.DELIVER_VALUE; return "Group updated.";
} }
public static String serializeArguments(byte[] id, String name, List<String> members) { try {
return Base64.encodeBytes(GroupContext.newBuilder() String description = "";
.setId(ByteString.copyFrom(id)) GroupContext context = GroupContext.parseFrom(Base64.decode(encodedGroup));
.setName(name) List<String> members = context.getMembersList();
.addAllMembers(members) String title = context.getName();
.build().toByteArray());
}
public static String serializeArguments(GroupContext context) { if (!members.isEmpty()) {
return Base64.encodeBytes(context.toByteArray()); description += org.whispersystems.textsecure.util.Util.join(members, ", ") + " joined the group.";
} }
public static List<String> getSerializedArgumentMembers(String serialized) { if (title != null && !title.trim().isEmpty()) {
if (serialized == null) { description += " Updated title to '" + title + "'.";
return new LinkedList<String>();
} }
try { return description;
GroupContext context = GroupContext.parseFrom(Base64.decode(serialized)); } catch (InvalidProtocolBufferException e) {
return context.getMembersList(); Log.w("GroupUtil", e);
return "Group updated.";
} catch (IOException e) { } catch (IOException e) {
Log.w("GroupUtil", e); Log.w("GroupUtil", e);
return new LinkedList<String>(); return "Group updated.";
} }
} }
} }

Loading…
Cancel
Save