diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index 5a0e3b546c..9e16a2eac1 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -6,10 +6,10 @@ import android.content.Intent; import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; +import android.util.Pair; import android.view.View; import android.widget.EditText; import android.widget.ImageView; @@ -19,31 +19,43 @@ import android.widget.TextView; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; +import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.components.PushRecipientsPanel; import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.transport.PushTransport; import org.thoughtcrime.securesms.util.ActionBarUtil; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.NotInDirectoryException; +import org.whispersystems.textsecure.push.PushAttachmentPointer; import org.whispersystems.textsecure.util.InvalidNumberException; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Set; 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; public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActivity { @@ -237,7 +249,15 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv avatarBmp.compress(Bitmap.CompressFormat.PNG, 100, stream); byteArray = stream.toByteArray(); } - handleCreatePushGroup(groupName.getText().toString(), byteArray, selectedContacts); + try { + handleCreatePushGroup(groupName.getText().toString(), byteArray, selectedContacts); + } catch (IOException e) { + // TODO Jake's gonna fill this in. + Log.w("GroupCreateActivity", e); + } catch (InvalidNumberException e) { + // TODO jake's gonna fill this in. + Log.w("GroupCreateActivity", e); + } return null; } @@ -334,11 +354,62 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv } } - private void handleCreatePushGroup(String groupName, byte[] avatar, Set members) { - //todo + private Pair> handleCreatePushGroup(String groupName, + byte[] avatar, + Set members) + throws IOException, InvalidNumberException + { + List memberE164Numbers = getE164Numbers(members); + PushTransport transport = new PushTransport(this, masterSecret); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this); + byte[] groupId = groupDatabase.allocateGroupId(); + AttachmentPointer avatarPointer = null; + + GroupContext.Builder builder = GroupContext.newBuilder() + .setId(ByteString.copyFrom(groupId)) + .setType(GroupContext.Type.CREATE) + .setName(groupName) + .addAllMembers(memberE164Numbers); + + if (avatar != null) { + PushAttachmentPointer pointer = transport.createAttachment("image/png", avatar); + avatarPointer = AttachmentPointer.newBuilder() + .setKey(ByteString.copyFrom(pointer.getKey())) + .setContentType(pointer.getContentType()) + .setId(pointer.getId()).build(); + builder.setAvatar(avatarPointer); + } + + List failures = transport.deliver(new LinkedList(members), builder.build()); + groupDatabase.create(groupId, TextSecurePreferences.getLocalNumber(this), groupName, + memberE164Numbers, avatarPointer, null); + + if (avatar != null) { + groupDatabase.updateAvatar(groupId, avatar); + } + + long threadId = threadDatabase.getThreadIdForGroup(GroupUtil.getEncodedId(groupId)); + + return new Pair>(threadId, failures); } - private void handleCreateMmsGroup(Set members) { - //todo + private long handleCreateMmsGroup(Set members) { + Recipients recipients = new Recipients(new LinkedList(members)); + return DatabaseFactory.getThreadDatabase(this) + .getThreadIdFor(recipients, + ThreadDatabase.DistributionTypes.CONVERSATION); + } + + private List getE164Numbers(Set recipients) + throws InvalidNumberException + { + List results = new LinkedList(); + + for (Recipient recipient : recipients) { + results.add(Util.canonicalizeNumber(this, recipient.getNumber())); + } + + return results; } } diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java index 5170e1226d..77849922f7 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java @@ -135,7 +135,9 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { DatabaseFactory.getIdentityDatabase(context) .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey()); - broadcastSecurityUpdateEvent(context, threadId); + if (threadId != -1) { + broadcastSecurityUpdateEvent(context, threadId); + } } @Override diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index ef06e04d81..9a21e6f76d 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -18,6 +18,8 @@ import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Util; import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; @@ -122,14 +124,17 @@ public class GroupDatabase extends Database { } public void updateAvatar(byte[] groupId, Bitmap avatar) { + updateAvatar(groupId, BitmapUtil.toByteArray(avatar)); + } + + public void updateAvatar(byte[] groupId, byte[] avatar) { ContentValues contentValues = new ContentValues(); - contentValues.put(AVATAR, BitmapUtil.toByteArray(avatar)); + contentValues.put(AVATAR, avatar); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(groupId)}); } - public void add(byte[] id, String source, List members) { List currentMembers = getCurrentMembers(id); @@ -177,6 +182,16 @@ public class GroupDatabase extends Database { } } + public byte[] allocateGroupId() { + try { + byte[] groupId = new byte[16]; + SecureRandom.getInstance("SHA1PRNG").nextBytes(groupId); + return groupId; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + public static class Reader { diff --git a/src/org/thoughtcrime/securesms/push/GroupActionRecord.java b/src/org/thoughtcrime/securesms/push/GroupActionRecord.java new file mode 100644 index 0000000000..c3fd3f6f94 --- /dev/null +++ b/src/org/thoughtcrime/securesms/push/GroupActionRecord.java @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.push; + +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.util.Set; + +public class GroupActionRecord { + + private static final int CREATE_GROUP_TYPE = 1; + private static final int ADD_USERS_TYPE = 2; + private static final int LEAVE_GROUP_TYPE = 3; + + private final int type; + private final Set recipients; + private final byte[] groupId; + private final String groupName; + private final byte[] avatar; + + public GroupActionRecord(int type, byte[] groupId, String groupName, + byte[] avatar, Set recipients) + { + this.type = type; + this.groupId = groupId; + this.groupName = groupName; + this.avatar = avatar; + this.recipients = recipients; + } + + public boolean isCreateAction() { + return type== CREATE_GROUP_TYPE; + } + + public boolean isAddUsersAction() { + return type == ADD_USERS_TYPE; + } + + public boolean isLeaveAction() { + return type == LEAVE_GROUP_TYPE; + } + + + public Set getRecipients() { + return recipients; + } + + public byte[] getGroupId() { + return groupId; + } + + public String getGroupName() { + return groupName; + } + + public byte[] getAvatar() { + return avatar; + } +} diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index 7aef3939c6..08db3558a0 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -123,6 +123,45 @@ public class PushTransport extends BaseTransport { } } + public List deliver(List recipients, + PushMessageContent.GroupContext groupAction) + throws IOException + { + PushServiceSocket socket = PushServiceSocketFactory.create(context); + byte[] plaintext = PushMessageContent.newBuilder() + .setGroup(groupAction) + .build().toByteArray(); + List failures = new LinkedList(); + + for (Recipient recipient : recipients) { + try { + deliver(socket, recipient, -1, plaintext); + } catch (UnregisteredUserException e) { + Log.w("PushTransport", e); + failures.add(recipient); + } catch (InvalidNumberException e) { + Log.w("PushTransport", e); + failures.add(recipient); + } catch (IOException e) { + Log.w("PushTransport", e); + failures.add(recipient); + } + } + + if (failures.size() == recipients.size()) { + throw new IOException("Total failure."); + } + + 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) throws IOException, InvalidNumberException { @@ -151,19 +190,26 @@ public class PushTransport extends BaseTransport { ContentType.isAudioType(contentType) || ContentType.isVideoType(contentType)) { - AttachmentCipher cipher = new AttachmentCipher(); - byte[] key = cipher.getCombinedKeyMaterial(); - byte[] ciphertextAttachment = cipher.encrypt(body.getPart(i).getData()); - PushAttachmentData attachmentData = new PushAttachmentData(contentType, ciphertextAttachment); - long attachmentId = socket.sendAttachment(attachmentData); - - attachments.add(new PushAttachmentPointer(contentType, attachmentId, key)); + attachments.add(getPushAttachmentPointer(socket, contentType, body.getPart(i).getData())); } } return attachments; } + private PushAttachmentPointer getPushAttachmentPointer(PushServiceSocket socket, + String contentType, byte[] data) + throws IOException + { + AttachmentCipher cipher = new AttachmentCipher(); + byte[] key = cipher.getCombinedKeyMaterial(); + byte[] ciphertextAttachment = cipher.encrypt(data); + PushAttachmentData attachmentData = new PushAttachmentData(contentType, ciphertextAttachment); + long attachmentId = socket.sendAttachment(attachmentData); + + return new PushAttachmentPointer(contentType, attachmentId, key); + } + private void handleMismatchedDevices(PushServiceSocket socket, long threadId, Recipient recipient, MismatchedDevices mismatchedDevices)