Handle identity key mismatch on outgoing group messages.

Additionally, make the group creation process asynchronous.
This commit is contained in:
Moxie Marlinspike 2014-02-17 11:42:51 -08:00
parent 5810062b25
commit b9f4fba98a
19 changed files with 355 additions and 156 deletions

View File

@ -5,7 +5,6 @@ import android.util.Log;
import com.google.thoughtcrimegson.Gson; import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException; import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonSyntaxException;
import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.IdentityKey;
@ -27,7 +26,6 @@ import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -94,7 +92,7 @@ public class PushServiceSocket {
try { try {
makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle)); makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
} catch (NotFoundException nfe) { } catch (NotFoundException nfe) {
throw new UnregisteredUserException(nfe); throw new UnregisteredUserException(bundle.getDestination(), nfe);
} }
} }

View File

@ -5,8 +5,14 @@ import java.util.List;
public class UnregisteredUserException extends IOException { public class UnregisteredUserException extends IOException {
public UnregisteredUserException(Exception exception) { private final String e164number;
public UnregisteredUserException(String e164number, Exception exception) {
super(exception); super(exception);
this.e164number = e164number;
} }
public String getE164Number() {
return e164number;
}
} }

View File

@ -34,6 +34,7 @@ import android.provider.ContactsContract.QuickContact;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
@ -42,6 +43,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@ -53,14 +55,17 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.Emoji; import org.thoughtcrime.securesms.util.Emoji;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.FutureTaskListener; import org.whispersystems.textsecure.util.FutureTaskListener;
import org.whispersystems.textsecure.util.ListenableFutureTask; import org.whispersystems.textsecure.util.ListenableFutureTask;
import org.whispersystems.textsecure.util.Util;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -172,13 +177,14 @@ public class ConversationItem extends LinearLayout {
/// MessageRecord Attribute Parsers /// MessageRecord Attribute Parsers
private void setBodyText(MessageRecord messageRecord) { private void setBodyText(MessageRecord messageRecord) {
// TODO jake is going to fill these in
switch (messageRecord.getGroupAction()) { switch (messageRecord.getGroupAction()) {
case GroupContext.Type.QUIT_VALUE: case GroupContext.Type.QUIT_VALUE:
bodyText.setText(messageRecord.getIndividualRecipient().toShortString() + " has left the group."); bodyText.setText(messageRecord.getIndividualRecipient().toShortString() + " has left the group.");
return; return;
case GroupContext.Type.ADD_VALUE: case GroupContext.Type.ADD_VALUE:
case GroupContext.Type.CREATE_VALUE: case GroupContext.Type.CREATE_VALUE:
bodyText.setText(messageRecord.getGroupActionArguments() + " have joined the group."); bodyText.setText(Util.join(GroupUtil.getSerializedArgumentMembers(messageRecord.getGroupActionArguments()), ", ") + " have joined the group.");
return; return;
case GroupContext.Type.MODIFY_VALUE: case GroupContext.Type.MODIFY_VALUE:
bodyText.setText(messageRecord.getIndividualRecipient() + " has updated the group."); bodyText.setText(messageRecord.getIndividualRecipient() + " has updated the group.");

View File

@ -9,7 +9,6 @@ import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.View; import android.view.View;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
@ -30,8 +29,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.transport.PushTransport;
import org.thoughtcrime.securesms.util.ActionBarUtil; import org.thoughtcrime.securesms.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
@ -42,7 +40,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.PushAttachmentPointer; import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -54,8 +52,9 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import ws.com.google.android.mms.MmsException;
import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -355,59 +354,32 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv
} }
} }
private Pair<Long, List<Recipient>> handleCreatePushGroup(String groupName, private long handleCreatePushGroup(String groupName,
byte[] avatar, byte[] avatar,
Set<Recipient> members) Set<Recipient> members)
throws IOException, InvalidNumberException throws IOException, InvalidNumberException
{ {
List<String> memberE164Numbers = getE164Numbers(members);
PushTransport transport = new PushTransport(this, masterSecret);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
byte[] groupId = groupDatabase.allocateGroupId();
AttachmentPointer avatarPointer = null;
memberE164Numbers.add(TextSecurePreferences.getLocalNumber(this));
GroupContext.Builder builder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(groupId))
.setType(GroupContext.Type.CREATE)
.setName(groupName)
.addAllMembers(memberE164Numbers);
if (avatar != null) {
PushAttachmentPointer pointer = transport.createAttachment("image/png", avatar);
avatarPointer = AttachmentPointer.newBuilder()
.setKey(ByteString.copyFrom(pointer.getKey()))
.setContentType(pointer.getContentType())
.setId(pointer.getId()).build();
builder.setAvatar(avatarPointer);
}
List<Recipient> failures = transport.deliver(new LinkedList<Recipient>(members), builder.build());
groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName,
memberE164Numbers, avatarPointer, null);
if (avatar != null) {
groupDatabase.updateAvatar(groupId, avatar);
}
try { try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
List<String> memberE164Numbers = getE164Numbers(members);
byte[] groupId = groupDatabase.allocateGroupId();
String groupRecipientId = GroupUtil.getEncodedId(groupId); String groupRecipientId = GroupUtil.getEncodedId(groupId);
Recipient groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false).getPrimaryRecipient();
OutgoingTextMessage outgoing = new OutgoingTextMessage(groupRecipient, GroupContext.Type.ADD_VALUE, org.whispersystems.textsecure.util.Util.join(memberE164Numbers, ","));
long threadId = threadDatabase.getThreadIdFor(new Recipients(groupRecipient));
List<Long> messageIds = DatabaseFactory.getEncryptingSmsDatabase(this)
.insertMessageOutbox(masterSecret, threadId, outgoing);
for (long messageId : messageIds) { String groupActionArguments = GroupUtil.serializeArguments(groupId, groupName, memberE164Numbers);
DatabaseFactory.getEncryptingSmsDatabase(this).markAsSent(messageId);
}
groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName,
memberE164Numbers, null, null);
groupDatabase.updateAvatar(groupId, avatar);
return new Pair<Long, List<Recipient>>(threadId, failures); Recipients groupRecipient = RecipientFactory.getRecipientsFromString(this, groupRecipientId, false);
return MessageSender.sendGroupAction(this, masterSecret, groupRecipient, -1,
GroupContext.Type.CREATE_VALUE,
groupActionArguments, avatar);
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
throw new AssertionError(e); throw new IOException(e);
} catch (MmsException e) {
throw new IOException(e);
} }
} }

View File

@ -132,7 +132,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION, CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION,
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS, MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT, RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT,
DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
GROUP_ACTION, GROUP_ACTION_ARGUMENTS
}; };
public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor(); public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor();
@ -344,8 +345,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
int groupAction = cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_ACTION));
String groupActionArguments = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ACTION_ARGUMENTS));
PduHeaders headers = getHeadersFromCursor(cursor); PduHeaders headers = getHeadersFromCursor(cursor);
addr.getAddressesForId(messageId, headers); addr.getAddressesForId(messageId, headers);
@ -361,7 +365,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
Log.w("MmsDatabase", e); Log.w("MmsDatabase", e);
} }
requests[i++] = new SendReq(headers, body, messageId, outboxType); requests[i++] = new SendReq(headers, body, messageId, outboxType, groupAction, groupActionArguments);
} }
return requests; return requests;
@ -514,6 +518,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
contentValues.put(THREAD_ID, threadId); contentValues.put(THREAD_ID, threadId);
contentValues.put(READ, 1); contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT)); contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.put(GROUP_ACTION, sendRequest.getGroupAction());
contentValues.put(GROUP_ACTION_ARGUMENTS, sendRequest.getGroupActionArguments());
contentValues.remove(ADDRESS); contentValues.remove(ADDRESS);
long messageId = insertMediaMessage(masterSecret, sendRequest.getPduHeaders(), long messageId = insertMediaMessage(masterSecret, sendRequest.getPduHeaders(),

View File

@ -20,13 +20,17 @@ import android.content.Context;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Pair;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.textsecure.push.PushMessageProtos; import org.whispersystems.textsecure.push.PushMessageProtos;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
/** /**
@ -55,12 +59,13 @@ public class ThreadRecord extends DisplayRecord {
@Override @Override
public SpannableString getDisplayBody() { public SpannableString getDisplayBody() {
// 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 (getGroupAction() == GroupContext.Type.ADD_VALUE || } else if (getGroupAction() == GroupContext.Type.ADD_VALUE ||
getGroupAction() == GroupContext.Type.CREATE_VALUE) getGroupAction() == GroupContext.Type.CREATE_VALUE)
{ {
return emphasisAdded("Added " + getGroupActionArguments()); return emphasisAdded(Util.join(GroupUtil.getSerializedArgumentMembers(getGroupActionArguments()), ", ") + " have joined the group");
} else if (getGroupAction() == GroupContext.Type.QUIT_VALUE) { } else if (getGroupAction() == GroupContext.Type.QUIT_VALUE) {
return emphasisAdded(getRecipients().toShortString() + " left the group."); return emphasisAdded(getRecipients().toShortString() + " left the group.");
} else if (getGroupAction() == GroupContext.Type.MODIFY_VALUE) { } else if (getGroupAction() == GroupContext.Type.MODIFY_VALUE) {

View File

@ -41,7 +41,7 @@ public class IncomingMediaMessage {
if (messageContent.hasGroup()) { if (messageContent.hasGroup()) {
this.groupId = GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray()); this.groupId = GroupUtil.getEncodedId(messageContent.getGroup().getId().toByteArray());
this.groupAction = messageContent.getGroup().getType().getNumber(); this.groupAction = messageContent.getGroup().getType().getNumber();
this.groupActionArguments = GroupUtil.getActionArgument(messageContent.getGroup()); this.groupActionArguments = GroupUtil.serializeArguments(messageContent.getGroup());
} else { } else {
this.groupId = null; this.groupId = null;
this.groupAction = -1; this.groupAction = -1;

View File

@ -92,8 +92,7 @@ public class MmsSender {
Recipients recipients = threads.getRecipientsForThreadId(threadId); Recipients recipients = threads.getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
} catch (UntrustedIdentityException uie) { } catch (UntrustedIdentityException uie) {
IncomingTextMessage base = new IncomingTextMessage(message); IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(uie.getIdentityKey().serialize()));
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage); DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId); database.markAsSentFailed(messageId);
} catch (RetryLaterException e) { } catch (RetryLaterException e) {

View File

@ -81,8 +81,7 @@ public class SmsSender {
transport.deliver(record); transport.deliver(record);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
Log.w("SmsSender", e); Log.w("SmsSender", e);
IncomingTextMessage base = new IncomingTextMessage(record); IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
IncomingIdentityUpdateMessage identityUpdateMessage = new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(e.getIdentityKey().serialize()));
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage); DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} catch (UndeliverableMessageException ude) { } catch (UndeliverableMessageException ude) {

View File

@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.sms; package org.thoughtcrime.securesms.sms;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.util.Base64;
public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage { public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage {
public IncomingIdentityUpdateMessage(IncomingTextMessage base, String newBody) { public IncomingIdentityUpdateMessage(IncomingTextMessage base, String newBody) {
@ -15,4 +18,13 @@ public class IncomingIdentityUpdateMessage extends IncomingKeyExchangeMessage {
public boolean isIdentityUpdate() { public boolean isIdentityUpdate() {
return true; return true;
} }
public static IncomingIdentityUpdateMessage createFor(String sender, IdentityKey identityKey) {
return createFor(sender, identityKey, null);
}
public static IncomingIdentityUpdateMessage createFor(String sender, IdentityKey identityKey, String groupId) {
IncomingTextMessage base = new IncomingTextMessage(sender, groupId, -1, null);
return new IncomingIdentityUpdateMessage(base, Base64.encodeBytesWithoutPadding(identityKey.serialize()));
}
} }

View File

@ -68,7 +68,7 @@ public class IncomingTextMessage implements Parcelable {
if (group != null) { if (group != null) {
this.groupId = GroupUtil.getEncodedId(group.getId().toByteArray()); this.groupId = GroupUtil.getEncodedId(group.getId().toByteArray());
this.groupAction = group.getType().getNumber(); this.groupAction = group.getType().getNumber();
this.groupActionArgument = GroupUtil.getActionArgument(group); this.groupActionArgument = GroupUtil.serializeArguments(group);
} else { } else {
this.groupId = null; this.groupId = null;
this.groupAction = -1; this.groupAction = -1;
@ -152,6 +152,22 @@ public class IncomingTextMessage implements Parcelable {
this.groupActionArgument = null; this.groupActionArgument = null;
} }
protected IncomingTextMessage(String sender, String groupId,
int groupAction, String groupActionArgument)
{
this.message = "";
this.sender = sender;
this.senderDeviceId = RecipientDevice.DEFAULT_DEVICE_ID;
this.protocol = 31338;
this.serviceCenterAddress = "Outgoing";
this.replyPathPresent = true;
this.pseudoSubject = "";
this.sentTimestampMillis = System.currentTimeMillis();
this.groupId = groupId;
this.groupAction = groupAction;
this.groupActionArgument = groupActionArgument;
}
public long getSentTimestampMillis() { public long getSentTimestampMillis() {
return sentTimestampMillis; return sentTimestampMillis;
} }
@ -235,4 +251,8 @@ public class IncomingTextMessage implements Parcelable {
out.writeInt(groupAction); out.writeInt(groupAction);
out.writeString(groupActionArgument); out.writeString(groupActionArgument);
} }
public static IncomingTextMessage createForLeavingGroup(String groupId, String user) {
return new IncomingTextMessage(user, groupId, GroupContext.Type.QUIT_VALUE, null);
}
} }

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
@ -34,10 +35,43 @@ import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduBody; import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq; import ws.com.google.android.mms.pdu.SendReq;
public class MessageSender { public class MessageSender {
public static long sendGroupAction(Context context, MasterSecret masterSecret, Recipients recipients,
long threadId, int groupAction, String groupActionArguments, byte[] avatar)
throws MmsException
{
if (threadId == -1) {
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
}
PduBody body = new PduBody();
if (avatar != null) {
PduPart part = new PduPart();
part.setData(avatar);
part.setContentType(ContentType.IMAGE_PNG.getBytes());
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Image" + System.currentTimeMillis()).getBytes());
body.addPart(part);
}
SendReq sendRequest = new SendReq();
sendRequest.setDate(System.currentTimeMillis() / 1000L);
sendRequest.setBody(body);
sendRequest.setContentType(ContentType.MULTIPART_MIXED.getBytes());
sendRequest.setGroupAction(groupAction);
sendRequest.setGroupActionArguments(groupActionArguments);
sendMms(context, recipients, masterSecret, sendRequest, threadId,
ThreadDatabase.DistributionTypes.CONVERSATION, true);
return threadId;
}
public static long sendMms(Context context, MasterSecret masterSecret, Recipients recipients, public static long sendMms(Context context, MasterSecret masterSecret, Recipients recipients,
long threadId, SlideDeck slideDeck, String message, int distributionType, long threadId, SlideDeck slideDeck, String message, int distributionType,
boolean secure) boolean secure)

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.transport;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import java.util.List;
public class EncapsulatedExceptions extends Throwable {
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
private final List<UnregisteredUserException> unregisteredUserExceptions;
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
List<UnregisteredUserException> unregisteredUsers)
{
this.untrustedIdentityExceptions = untrustedIdentities;
this.unregisteredUserExceptions = unregisteredUsers;
}
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
return untrustedIdentityExceptions;
}
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
return unregisteredUserExceptions;
}
}

View File

@ -51,6 +51,7 @@ import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException; import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionRecordV2; import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
@ -62,6 +63,8 @@ import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.SendReq; import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal; import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
public class PushTransport extends BaseTransport { public class PushTransport extends BaseTransport {
@ -100,7 +103,7 @@ public class PushTransport extends BaseTransport {
} }
public void deliver(SendReq message, long threadId) public void deliver(SendReq message, long threadId)
throws IOException, UntrustedIdentityException throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{ {
PushServiceSocket socket = PushServiceSocketFactory.create(context); PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = getPlaintextMessage(socket, message); byte[] plaintext = getPlaintextMessage(socket, message);
@ -108,7 +111,6 @@ public class PushTransport extends BaseTransport {
Recipients recipients; Recipients recipients;
try {
if (GroupUtil.isEncodedGroup(destination)) { if (GroupUtil.isEncodedGroup(destination)) {
recipients = DatabaseFactory.getGroupDatabase(context) recipients = DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(GroupUtil.getDecodedId(destination)); .getGroupMembers(GroupUtil.getDecodedId(destination));
@ -116,59 +118,24 @@ public class PushTransport extends BaseTransport {
recipients = RecipientFactory.getRecipientsFromString(context, destination, false); recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
} }
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<UntrustedIdentityException>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<UnregisteredUserException>();
for (Recipient recipient : recipients.getRecipientsList()) { for (Recipient recipient : recipients.getRecipientsList()) {
deliver(socket, recipient, threadId, plaintext);
}
} catch (UnregisteredUserException uue) {
// TODO: We should probably remove the user from the directory?
throw new IOException(uue);
} catch (RecipientFormattingException e) {
throw new IOException(e);
} catch (InvalidNumberException e) {
throw new IOException(e);
}
}
public List<Recipient> deliver(List<Recipient> recipients,
PushMessageContent.GroupContext groupAction)
throws IOException
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
byte[] plaintext = PushMessageContent.newBuilder()
.setGroup(groupAction)
.build().toByteArray();
List<Recipient> failures = new LinkedList<Recipient>();
for (Recipient recipient : recipients) {
try { try {
deliver(socket, recipient, -1, plaintext); deliver(socket, recipient, threadId, plaintext);
} catch (UnregisteredUserException e) {
Log.w("PushTransport", e);
failures.add(recipient);
} catch (InvalidNumberException e) {
Log.w("PushTransport", e);
failures.add(recipient);
} catch (IOException e) {
Log.w("PushTransport", e);
failures.add(recipient);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
Log.w("PushTransport", e); Log.w("PushTransport", e);
failures.add(recipient); untrustedIdentities.add(e);
} catch (UnregisteredUserException e) {
Log.w("PushTransport", e);
unregisteredUsers.add(e);
} }
} }
if (failures.size() == recipients.size()) { if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
throw new IOException("Total failure."); throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
} }
return failures;
}
public PushAttachmentPointer createAttachment(String contentType, byte[] data)
throws IOException
{
PushServiceSocket socket = PushServiceSocketFactory.create(context);
return getPushAttachmentPointer(socket, contentType, data);
} }
private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext) private void deliver(PushServiceSocket socket, Recipient recipient, long threadId, byte[] plaintext)
@ -252,11 +219,40 @@ public class PushTransport extends BaseTransport {
PushMessageContent.Builder builder = PushMessageContent.newBuilder(); PushMessageContent.Builder builder = PushMessageContent.newBuilder();
if (GroupUtil.isEncodedGroup(message.getTo()[0].getString())) { if (GroupUtil.isEncodedGroup(message.getTo()[0].getString())) {
PushMessageContent.GroupContext.Builder groupBuilder = byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
PushMessageContent.GroupContext.newBuilder(); GroupContext.Builder groupBuilder = GroupContext.newBuilder();
groupBuilder.setType(PushMessageContent.GroupContext.Type.DELIVER); groupBuilder.setId(ByteString.copyFrom(groupId));
groupBuilder.setId(ByteString.copyFrom(GroupUtil.getDecodedId(message.getTo()[0].getString())));
switch (message.getGroupAction()) {
case GroupContext.Type.ADD_VALUE: groupBuilder.setType(GroupContext.Type.ADD); break;
case GroupContext.Type.CREATE_VALUE: groupBuilder.setType(GroupContext.Type.CREATE); break;
case GroupContext.Type.QUIT_VALUE: groupBuilder.setType(GroupContext.Type.QUIT); break;
default: groupBuilder.setType(GroupContext.Type.DELIVER); break;
}
if (message.getGroupAction() == GroupContext.Type.ADD_VALUE ||
message.getGroupAction() == GroupContext.Type.CREATE_VALUE)
{
GroupContext serialized = GroupContext.parseFrom(Base64.decode(message.getGroupActionArguments()));
groupBuilder.addAllMembers(serialized.getMembersList());
if (serialized.hasName()) {
groupBuilder.setName(serialized.getName());
}
}
if (message.getGroupAction() == GroupContext.Type.CREATE_VALUE && !attachments.isEmpty()) {
Log.w("PushTransport", "Adding avatar...");
groupBuilder.setAvatar(AttachmentPointer.newBuilder()
.setId(attachments.get(0).getId())
.setContentType(attachments.get(0).getContentType())
.setKey(ByteString.copyFrom(attachments.get(0).getKey()))
.build());
attachments.remove(0);
} else {
Log.w("PushTransport", "Not adding avatar: " + message.getGroupAction() + " , " + attachments.isEmpty());
}
builder.setGroup(groupBuilder.build()); builder.setGroup(groupBuilder.build());
} }
@ -266,8 +262,8 @@ public class PushTransport extends BaseTransport {
} }
for (PushAttachmentPointer attachment : attachments) { for (PushAttachmentPointer attachment : attachments) {
PushMessageContent.AttachmentPointer.Builder attachmentBuilder = AttachmentPointer.Builder attachmentBuilder =
PushMessageContent.AttachmentPointer.newBuilder(); AttachmentPointer.newBuilder();
attachmentBuilder.setId(attachment.getId()); attachmentBuilder.setId(attachment.getId());
attachmentBuilder.setContentType(attachment.getContentType()); attachmentBuilder.setContentType(attachment.getContentType());
@ -316,7 +312,7 @@ public class PushTransport extends BaseTransport {
if (processor.isTrusted(preKey)) { if (processor.isTrusted(preKey)) {
processor.processKeyExchangeMessage(preKey, threadId); processor.processKeyExchangeMessage(preKey, threadId);
} else { } else {
throw new UntrustedIdentityException("Untrusted identity key!", preKey.getIdentityKey()); throw new UntrustedIdentityException("Untrusted identity key!", pushAddress.getNumber(), preKey.getIdentityKey());
} }
} }
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {

View File

@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.transport; package org.thoughtcrime.securesms.transport;
import java.io.IOException;
public class RetryLaterException extends Exception { public class RetryLaterException extends Exception {
public RetryLaterException(Exception e) {
super(e);
}
} }

View File

@ -19,10 +19,14 @@ package org.thoughtcrime.securesms.transport;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory; import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -31,7 +35,11 @@ import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactNumberDetails; import org.whispersystems.textsecure.push.ContactNumberDetails;
import org.whispersystems.textsecure.push.ContactTokenDetails; import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos;
import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.DirectoryUtil; import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
@ -39,15 +47,19 @@ import java.io.IOException;
import ws.com.google.android.mms.pdu.SendReq; import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
public class UniversalTransport { public class UniversalTransport {
private final Context context; private final Context context;
private final MasterSecret masterSecret;
private final PushTransport pushTransport; private final PushTransport pushTransport;
private final SmsTransport smsTransport; private final SmsTransport smsTransport;
private final MmsTransport mmsTransport; private final MmsTransport mmsTransport;
public UniversalTransport(Context context, MasterSecret masterSecret) { public UniversalTransport(Context context, MasterSecret masterSecret) {
this.context = context; this.context = context;
this.masterSecret = masterSecret;
this.pushTransport = new PushTransport(context, masterSecret); this.pushTransport = new PushTransport(context, masterSecret);
this.smsTransport = new SmsTransport(context, masterSecret); this.smsTransport = new SmsTransport(context, masterSecret);
this.mmsTransport = new MmsTransport(context, masterSecret); this.mmsTransport = new MmsTransport(context, masterSecret);
@ -90,6 +102,10 @@ public class UniversalTransport {
throw new UndeliverableMessageException("No destination specified"); throw new UndeliverableMessageException("No destination specified");
} }
if (GroupUtil.isEncodedGroup(mediaMessage.getTo()[0].getString())) {
return deliverGroupMessage(mediaMessage, threadId);
}
if (!TextSecurePreferences.isPushRegistered(context)) { if (!TextSecurePreferences.isPushRegistered(context)) {
return mmsTransport.deliver(mediaMessage); return mmsTransport.deliver(mediaMessage);
} }
@ -98,14 +114,8 @@ public class UniversalTransport {
return mmsTransport.deliver(mediaMessage); return mmsTransport.deliver(mediaMessage);
} }
String destination;
try { try {
destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString()); String destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
} catch (InvalidNumberException ine) {
Log.w("UniversalTransport", ine);
return mmsTransport.deliver(mediaMessage);
}
if (isPushTransport(destination)) { if (isPushTransport(destination)) {
try { try {
@ -114,17 +124,66 @@ public class UniversalTransport {
return new MmsSendResult("push".getBytes("UTF-8"), 0, true); return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w("UniversalTransport", ioe); Log.w("UniversalTransport", ioe);
if (!GroupUtil.isEncodedGroup(destination)) { return mmsTransport.deliver(mediaMessage);
} catch (RecipientFormattingException e) {
Log.w("UniversalTransport", e);
return mmsTransport.deliver(mediaMessage);
} catch (EncapsulatedExceptions ee) {
Log.w("UniversalTransport", ee);
if (!ee.getUnregisteredUserExceptions().isEmpty()) {
return mmsTransport.deliver(mediaMessage); return mmsTransport.deliver(mediaMessage);
} else { } else {
throw new RetryLaterException(); throw new UntrustedIdentityException(ee.getUntrustedIdentityExceptions().get(0));
} }
} }
} else { } else {
Log.w("UniversalTransport", "Delivering media message with MMS..."); Log.w("UniversalTransport", "Delivering media message with MMS...");
return mmsTransport.deliver(mediaMessage); return mmsTransport.deliver(mediaMessage);
} }
} catch (InvalidNumberException ine) {
Log.w("UniversalTransport", ine);
return mmsTransport.deliver(mediaMessage);
} }
}
private MmsSendResult deliverGroupMessage(SendReq mediaMessage, long threadId)
throws RetryLaterException, UndeliverableMessageException
{
if (!TextSecurePreferences.isPushRegistered(context)) {
throw new UndeliverableMessageException("Not push registered!");
}
try {
pushTransport.deliver(mediaMessage, threadId);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException e) {
Log.w("UniversalTransport", e);
throw new RetryLaterException(e);
} catch (RecipientFormattingException e) {
throw new UndeliverableMessageException(e);
} catch (InvalidNumberException e) {
throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w("UniversalTransport", ee);
try {
for (UnregisteredUserException unregistered : ee.getUnregisteredUserExceptions()) {
IncomingTextMessage quitMessage = IncomingTextMessage.createForLeavingGroup(mediaMessage.getTo()[0].getString(), unregistered.getE164Number());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, quitMessage);
DatabaseFactory.getGroupDatabase(context).remove(GroupUtil.getDecodedId(mediaMessage.getTo()[0].getString()), unregistered.getE164Number());
}
for (UntrustedIdentityException untrusted : ee.getUntrustedIdentityExceptions()) {
IncomingIdentityUpdateMessage identityMessage = IncomingIdentityUpdateMessage.createFor(untrusted.getE164Number(), untrusted.getIdentityKey(), mediaMessage.getTo()[0].getString());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityMessage);
}
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
}
public boolean isMultipleRecipients(SendReq mediaMessage) { public boolean isMultipleRecipients(SendReq mediaMessage) {
int recipientCount = 0; int recipientCount = 0;

View File

@ -1,17 +1,29 @@
package org.thoughtcrime.securesms.transport; package org.thoughtcrime.securesms.transport;
import org.whispersystems.textsecure.crypto.IdentityKey; import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.push.UnregisteredUserException;
public class UntrustedIdentityException extends Exception { public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey; private final IdentityKey identityKey;
private final String e164number;
public UntrustedIdentityException(String s, IdentityKey identityKey) { public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
super(s); super(s);
this.e164number = e164number;
this.identityKey = identityKey; this.identityKey = identityKey;
} }
public UntrustedIdentityException(UntrustedIdentityException e) {
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
}
public IdentityKey getIdentityKey() { public IdentityKey getIdentityKey() {
return identityKey; return identityKey;
} }
public String getE164Number() {
return e164number;
}
} }

View File

@ -1,8 +1,16 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import android.util.Log;
import android.util.Pair;
import com.google.protobuf.ByteString;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -26,15 +34,29 @@ public class GroupUtil {
return groupId.startsWith(ENCODED_GROUP_PREFIX); return groupId.startsWith(ENCODED_GROUP_PREFIX);
} }
public static String getActionArgument(GroupContext group) { public static String serializeArguments(byte[] id, String name, List<String> members) {
if (group.getType().equals(GroupContext.Type.CREATE) || return Base64.encodeBytes(GroupContext.newBuilder()
group.getType().equals(GroupContext.Type.ADD)) .setId(ByteString.copyFrom(id))
{ .setName(name)
return org.whispersystems.textsecure.util.Util.join(group.getMembersList(), ","); .addAllMembers(members)
} else if (group.getType().equals(GroupContext.Type.MODIFY)) { .build().toByteArray());
return group.getName();
} }
return null; 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>();
}
try {
GroupContext context = GroupContext.parseFrom(Base64.decode(serialized));
return context.getMembersList();
} catch (IOException e) {
Log.w("GroupUtil", e);
return new LinkedList<String>();
}
} }
} }

View File

@ -25,6 +25,8 @@ public class SendReq extends MultimediaMessagePdu {
private static final String TAG = "SendReq"; private static final String TAG = "SendReq";
private long databaseMessageId; private long databaseMessageId;
private long messageBox; private long messageBox;
private int groupAction;
private String groupActionArguments;
public SendReq() { public SendReq() {
super(); super();
@ -90,10 +92,14 @@ public class SendReq extends MultimediaMessagePdu {
super(headers, body); super(headers, body);
} }
public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox) { public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox,
int groupAction, String groupActionArguments)
{
super(headers, body); super(headers, body);
this.databaseMessageId = messageId; this.databaseMessageId = messageId;
this.messageBox = messageBox; this.messageBox = messageBox;
this.groupAction = groupAction;
this.groupActionArguments = groupActionArguments;
} }
public long getDatabaseMessageBox() { public long getDatabaseMessageBox() {
@ -104,6 +110,22 @@ public class SendReq extends MultimediaMessagePdu {
return databaseMessageId; return databaseMessageId;
} }
public int getGroupAction() {
return this.groupAction;
}
public String getGroupActionArguments() {
return this.groupActionArguments;
}
public void setGroupAction(int groupAction) {
this.groupAction = groupAction;
}
public void setGroupActionArguments(String groupActionArguments) {
this.groupActionArguments = groupActionArguments;
}
/** /**
* Get Bcc value. * Get Bcc value.
* *