Refactor group messaging protocol.

// FREEBIE
master
Moxie Marlinspike 2014-02-21 17:51:25 -08:00
parent b855f8a163
commit a6e1d56cde
18 changed files with 314 additions and 254 deletions

View File

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

View File

@ -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() {

View File

@ -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) {
final List<String> e164numbers = new ArrayList<String>(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<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() {
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<Long, Recipients> handleCreatePushGroup(String groupName,
byte[] avatar,
private Pair<Long, Recipients> handleCreatePushGroup(String groupName, byte[] avatar,
Set<Recipient> members)
throws InvalidNumberException, MmsException
{
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
byte[] groupId = groupDatabase.allocateGroupId();
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
byte[] groupId = groupDatabase.allocateGroupId();
List<String> 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<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 {
List<String> 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<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 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();

View File

@ -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);

View File

@ -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();

View File

@ -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) {

View File

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

View File

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

View File

@ -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());

View File

@ -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)) {

View File

@ -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;
}
}

View File

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

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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<Long, Long> 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)

View File

@ -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()

View File

@ -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) {

View File

@ -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<String> 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<String> getSerializedArgumentMembers(String serialized) {
if (serialized == null) {
return new LinkedList<String>();
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<String> 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<String>();
return "Group updated.";
}
}
}