From 9c9866e7ee7c3a19078d61562ced90f33eb74296 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sat, 22 Feb 2014 10:54:43 -0800 Subject: [PATCH] Add 'leave group' functionality. Includes other bug fixes. --- res/values/strings.xml | 2 + .../securesms/ConversationActivity.java | 119 ++++++++++++++++-- .../securesms/database/DatabaseFactory.java | 2 +- .../securesms/database/GroupDatabase.java | 32 ++++- .../securesms/database/PushDatabase.java | 1 + .../database/model/MessageRecord.java | 2 + .../database/model/ThreadRecord.java | 2 +- .../securesms/gcm/GcmIntentService.java | 5 + .../mms/OutgoingGroupMediaMessage.java | 14 ++- .../securesms/sms/OutgoingTextMessage.java | 29 +---- .../securesms/util/GroupUtil.java | 6 +- 11 files changed, 159 insertions(+), 55 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 786a7454a0..d3f9d849a7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -78,6 +78,8 @@ Invalid recipient! Calls Not Supported This device does not appear to support dial actions. + Leave group? + Are you sure you want to leave this group? Message details diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index c72d03f645..ce74e0b9f4 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -54,6 +54,7 @@ import android.widget.Toast; 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.EmojiDrawer; import org.thoughtcrime.securesms.components.EmojiToggle; @@ -65,11 +66,13 @@ import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase.Draft; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter; import org.thoughtcrime.securesms.mms.MediaTooLargeException; import org.thoughtcrime.securesms.mms.MmsSendHelper; +import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MessageNotifier; @@ -92,14 +95,19 @@ import org.thoughtcrime.securesms.util.CharacterCalculator; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.directory.Directory; +import org.whispersystems.textsecure.directory.NotInDirectoryException; +import org.whispersystems.textsecure.push.PushMessageProtos; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.Session; import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.Util; import java.io.IOException; @@ -108,6 +116,9 @@ import java.util.List; import ws.com.google.android.mms.MmsException; +import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; +import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; + /** * Activity for displaying a message thread, as well as * composing/sending a new message into that thread. @@ -190,6 +201,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi initializeSecurity(); initializeTitleBar(); + initializeEnabledCheck(); initializeMmsEnabledCheck(); initializeIme(); calculateCharactersRemaining(); @@ -275,7 +287,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } else { menu.findItem(R.id.menu_distribution_conversation).setChecked(true); } - } else { + } else if (isActiveGroup()) { inflater.inflate(R.menu.conversation_push_group_options, menu); } } @@ -307,17 +319,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi return false; } - private void handleLeavePushGroup() { - Toast.makeText(getApplicationContext(), "not yet implemented", Toast.LENGTH_SHORT).show(); - } - - private void handleEditPushGroup() { - Intent intent = new Intent(ConversationActivity.this, GroupCreateActivity.class); - intent.putExtra(GroupCreateActivity.MASTER_SECRET_EXTRA, masterSecret); - intent.putExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA, recipients); - startActivityForResult(intent, GROUP_EDIT); - } - @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { if (isEncryptedConversation) { @@ -425,6 +426,57 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi builder.show(); } + private void handleLeavePushGroup() { + if (getRecipients() == null) { + Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), + Toast.LENGTH_LONG).show(); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.ConversationActivity_leave_group)); + builder.setIcon(android.R.drawable.ic_dialog_info); + builder.setCancelable(true); + builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)); + builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + Context self = ConversationActivity.this; + byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber()); + DatabaseFactory.getGroupDatabase(self).setActive(groupId, false); + + GroupContext context = GroupContext.newBuilder() + .setId(ByteString.copyFrom(groupId)) + .setType(GroupContext.Type.QUIT) + .build(); + + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(self, getRecipients(), + context, null); + MessageSender.send(self, masterSecret, outgoingMessage, threadId); + initializeEnabledCheck(); + } catch (IOException e) { + Log.w(TAG, e); + Toast.makeText(ConversationActivity.this, "Error leaving group....", Toast.LENGTH_LONG); + } catch (MmsException e) { + Log.w(TAG, e); + Toast.makeText(ConversationActivity.this, "Error leaving group...", Toast.LENGTH_LONG); + } + } + }); + + builder.setNegativeButton(R.string.no, null); + builder.show(); + } + + private void handleEditPushGroup() { + Intent intent = new Intent(ConversationActivity.this, GroupCreateActivity.class); + intent.putExtra(GroupCreateActivity.MASTER_SECRET_EXTRA, masterSecret); + intent.putExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA, recipients); + startActivityForResult(intent, GROUP_EDIT); + } + + private void handleDistributionBroadcastEnabled(MenuItem item) { distributionType = ThreadDatabase.DistributionTypes.BROADCAST; item.setChecked(true); @@ -502,7 +554,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } private void handleAddAttachment() { - if (this.isMmsEnabled) { + if (this.isMmsEnabled || isPushDestination()) { AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.TextSecure_Light_Dialog)); builder.setIcon(R.drawable.ic_dialog_attach); builder.setTitle(R.string.ConversationActivity_add_attachment); @@ -577,6 +629,12 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } } + private void initializeEnabledCheck() { + boolean enabled = !(isGroupConversation() && !isActiveGroup()); + composeText.setEnabled(enabled); + sendButton.setEnabled(enabled); + } + private void initializeDraftFromDatabase() { new AsyncTask>() { @Override @@ -882,6 +940,20 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi return getRecipients() != null && getRecipients().isSingleRecipient() && !getRecipients().isGroupRecipient(); } + private boolean isActiveGroup() { + if (!isGroupConversation()) return false; + + try { + byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber()); + GroupRecord record = DatabaseFactory.getGroupDatabase(this).getGroup(groupId); + + return record.isActive(); + } catch (IOException e) { + Log.w("ConversationActivity", e); + return false; + } + } + private boolean isGroupConversation() { return getRecipients() != null && (!getRecipients().isSingleRecipient() || getRecipients().isGroupRecipient()); @@ -891,6 +963,27 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi return getRecipients().isGroupRecipient(); } + private boolean isPushDestination() { + try { + if (!TextSecurePreferences.isPushRegistered(this)) + return false; + + if (isPushGroupConversation()) + return true; + + String number = getRecipients().getPrimaryRecipient().getNumber(); + String e164number = org.thoughtcrime.securesms.util.Util.canonicalizeNumber(this, number); + + return Directory.getInstance(this).isActiveNumber(e164number); + } catch (InvalidNumberException e) { + Log.w(TAG, e); + return false; + } catch (NotInDirectoryException e) { + Log.w(TAG, e); + return false; + } + } + private Recipients getRecipients() { try { if (isExistingConversation()) return this.recipients; diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 9f1b014bb7..6bd9daacf9 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -642,7 +642,7 @@ public class DatabaseFactory { } if (oldVersion < INTRODUCED_GROUP_DATABASE_VERSION) { - db.execSQL("CREATE TABLE groups (_id INTEGER PRIMARY KEY, group_id TEXT, owner TEXT, title TEXT, members TEXT, avatar BLOB, avatar_id INTEGER, avatar_key BLOB, avatar_content_type TEXT, avatar_relay TEXT, timestamp INTEGER);"); + db.execSQL("CREATE TABLE groups (_id INTEGER PRIMARY KEY, group_id TEXT, owner TEXT, title TEXT, members TEXT, avatar BLOB, avatar_id INTEGER, avatar_key BLOB, avatar_content_type TEXT, avatar_relay TEXT, timestamp INTEGER, active INTEGER DEFAULT 1);"); db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON groups (GROUP_ID);"); db.execSQL("ALTER TABLE push ADD COLUMN device_id INTEGER DEFAULT 1;"); db.execSQL("ALTER TABLE sms ADD COLUMN address_device_id INTEGER DEFAULT 1;"); diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 5dcd78b7e0..f843868b0d 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -16,14 +16,11 @@ import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; -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.ArrayList; -import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -43,6 +40,7 @@ public class GroupDatabase extends Database { private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; private static final String AVATAR_RELAY = "avatar_relay"; private static final String TIMESTAMP = "timestamp"; + private static final String ACTIVE = "active"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + @@ -56,7 +54,8 @@ public class GroupDatabase extends Database { AVATAR_KEY + " BLOB, " + AVATAR_CONTENT_TYPE + " TEXT, " + AVATAR_RELAY + " TEXT, " + - TIMESTAMP + " INTEGER);"; + TIMESTAMP + " INTEGER, " + + ACTIVE + " INTEGER DEFAULT 1);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", @@ -125,6 +124,7 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_RELAY, relay); contentValues.put(TIMESTAMP, System.currentTimeMillis()); + contentValues.put(ACTIVE, 1); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); } @@ -173,6 +173,7 @@ public class GroupDatabase extends Database { ContentValues contents = new ContentValues(); contents.put(MEMBERS, Util.join(concatenatedMembers, ",")); + contents.put(ACTIVE, 1); databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(id)}); @@ -211,6 +212,18 @@ public class GroupDatabase extends Database { } } + public boolean isActive(byte[] id) { + GroupRecord record = getGroup(id); + return record != null && record.isActive(); + } + + public void setActive(byte[] id, boolean active) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(ACTIVE, active ? 1 : 0); + database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(id)}); + } + public String getOwner(byte[] id) { Cursor cursor = null; @@ -265,7 +278,8 @@ public class GroupDatabase extends Database { cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), - cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY))); + cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)), + cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1); } public void close() { @@ -285,10 +299,11 @@ public class GroupDatabase extends Database { private final byte[] avatarKey; private final String avatarContentType; private final String relay; + private final boolean active; public GroupRecord(String id, String title, String owner, String members, byte[] avatar, long avatarId, byte[] avatarKey, String avatarContentType, - String relay) + String relay, boolean active) { this.id = id; this.title = title; @@ -299,6 +314,7 @@ public class GroupDatabase extends Database { this.avatarKey = avatarKey; this.avatarContentType = avatarContentType; this.relay = relay; + this.active = active; } public byte[] getId() { @@ -340,5 +356,9 @@ public class GroupDatabase extends Database { public String getRelay() { return relay; } + + public boolean isActive() { + return active; + } } } diff --git a/src/org/thoughtcrime/securesms/database/PushDatabase.java b/src/org/thoughtcrime/securesms/database/PushDatabase.java index a4f6552a34..2db297828d 100644 --- a/src/org/thoughtcrime/securesms/database/PushDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PushDatabase.java @@ -33,6 +33,7 @@ public class PushDatabase extends Database { ContentValues values = new ContentValues(); values.put(TYPE, message.getType()); values.put(SOURCE, message.getSource()); + values.put(DEVICE_ID, message.getSourceDevice()); values.put(BODY, Base64.encodeBytes(message.getBody())); values.put(TIMESTAMP, message.getTimestampMillis()); diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index a196af93a8..67f5973ea0 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -86,6 +86,8 @@ public abstract class MessageRecord extends DisplayRecord { public SpannableString getDisplayBody() { if (isGroupUpdate()) { return emphasisAdded(GroupUtil.getDescription(getBody().getBody())); + } else if (isGroupQuit() && isOutgoing()) { + return emphasisAdded("You have left the group."); } else if (isGroupQuit()) { return emphasisAdded(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString())); } diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index a87b0403d5..c67e2cde2e 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -59,7 +59,7 @@ public class ThreadRecord extends DisplayRecord { } else if (isGroupUpdate()) { return emphasisAdded(GroupUtil.getDescription(getBody().getBody())); } else if (isGroupQuit()) { - return emphasisAdded(getRecipients().toShortString() + " left the group."); + return emphasisAdded("Someone left the group."); } else if (isKeyExchange()) { return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message)); } else if (SmsDatabase.Types.isFailedDecryptType(type)) { diff --git a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java index 93f643b712..2211cbb577 100644 --- a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java +++ b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java @@ -61,6 +61,11 @@ public class GcmIntentService extends GCMBaseIntentService { if (Util.isEmpty(data)) return; + if (!TextSecurePreferences.isPushRegistered(context)) { + Log.w("GcmIntentService", "Not push registered!"); + return; + } + String sessionKey = TextSecurePreferences.getSignalingKey(context); IncomingEncryptedPushMessage encryptedMessage = new IncomingEncryptedPushMessage(data, sessionKey); IncomingPushMessage message = encryptedMessage.getIncomingPushMessage(); diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index 57435fd5b8..332b4c07e6 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -24,12 +24,14 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { this.group = group; - 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); + 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); + } } @Override diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java index fe926b8838..fa35126451 100644 --- a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java @@ -8,32 +8,19 @@ public class OutgoingTextMessage { private final Recipients recipients; private final String message; - private final int groupAction; - private final String groupActionArguments; public OutgoingTextMessage(Recipient recipient, String message) { this(new Recipients(recipient), message); } public OutgoingTextMessage(Recipients recipients, String message) { - this.recipients = recipients; - this.message = message; - this.groupAction = -1; - this.groupActionArguments = null; - } - - public OutgoingTextMessage(Recipient recipient, int groupAction, String groupActionArguments) { - this.recipients = new Recipients(recipient); - this.groupAction = groupAction; - this.groupActionArguments = groupActionArguments; - this.message = ""; + this.recipients = recipients; + this.message = message; } protected OutgoingTextMessage(OutgoingTextMessage base, String body) { - this.recipients = base.getRecipients(); - this.groupAction = base.getGroupAction(); - this.groupActionArguments = base.getGroupActionArguments(); - this.message = body; + this.recipients = base.getRecipients(); + this.message = body; } public String getMessageBody() { @@ -75,12 +62,4 @@ public class OutgoingTextMessage { public OutgoingTextMessage withBody(String body) { return new OutgoingTextMessage(this, body); } - - public int getGroupAction() { - return groupAction; - } - - public String getGroupActionArguments() { - return groupActionArguments; - } } diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 1809193506..cca3513048 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -41,9 +41,9 @@ public class GroupUtil { try { String description = ""; - GroupContext context = GroupContext.parseFrom(Base64.decode(encodedGroup)); - List members = context.getMembersList(); - String title = context.getName(); + GroupContext context = GroupContext.parseFrom(Base64.decode(encodedGroup)); + List members = context.getMembersList(); + String title = context.getName(); if (!members.isEmpty()) { description += org.whispersystems.textsecure.util.Util.join(members, ", ") + " joined the group.";