From 0dc869228b49515bb885cec73852aa63478130e5 Mon Sep 17 00:00:00 2001 From: akwizgran <akwizgran@users.sourceforge.net> Date: Thu, 19 Dec 2013 21:53:26 +0000 Subject: [PATCH] Replaced private messages with private groups. Private messages are now the same as group messages, but groups can be private or public. When a contact is added, a private group is created and designated as the inbox for exchanging private messages with the contact. --- briar-android/res/values/strings.xml | 1 + .../android/DescendingHeaderComparator.java | 19 - .../net/sf/briar/android/SetupActivity.java | 10 +- .../android/contact/ContactListActivity.java | 110 +- .../android/contact/ContactListItem.java | 36 +- .../contact/ContactSpinnerAdapter.java | 77 -- .../android/contact/ConversationActivity.java | 56 +- .../android/contact/ConversationAdapter.java | 14 +- .../android/contact/ConversationItem.java | 37 + .../contact/ConversationItemComparator.java | 19 + .../contact/ReadPrivateMessageActivity.java | 6 + .../contact/WritePrivateMessageActivity.java | 167 +-- .../groups/ConfigureGroupActivity.java | 6 +- .../android/groups/CreateGroupActivity.java | 7 +- .../briar/android/groups/GroupActivity.java | 19 +- .../sf/briar/android/groups/GroupAdapter.java | 8 +- .../android/groups/GroupListActivity.java | 45 +- .../briar/android/groups/GroupListItem.java | 27 +- .../android/groups/ManageGroupsActivity.java | 25 +- .../groups/WriteGroupPostActivity.java | 11 +- .../identity/CreateIdentityActivity.java | 10 +- .../src/net/sf/briar/api/AuthorFactory.java | 6 +- .../sf/briar/api/crypto/CryptoComponent.java | 3 + .../sf/briar/api/db/DatabaseComponent.java | 110 +- .../sf/briar/api/db/GroupMessageHeader.java | 22 - .../net/sf/briar/api/db/MessageHeader.java | 16 +- .../sf/briar/api/db/PrivateMessageHeader.java | 32 - .../api/db/event/GroupMessageAddedEvent.java | 23 - .../api/db/event/LocalAuthorAddedEvent.java | 2 +- .../api/db/event/LocalAuthorRemovedEvent.java | 2 +- .../briar/api/db/event/MessageAddedEvent.java | 29 + .../db/event/PrivateMessageAddedEvent.java | 25 - .../src/net/sf/briar/api/messaging/Group.java | 9 +- .../sf/briar/api/messaging/GroupFactory.java | 6 +- .../briar/api/messaging/MessageFactory.java | 7 - .../api/messaging/MessagingConstants.java | 2 +- .../api/messaging/SubscriptionUpdate.java | 8 +- .../api/messaging/UnverifiedMessage.java | 16 +- .../sf/briar/crypto/CryptoComponentImpl.java | 9 + briar-core/src/net/sf/briar/db/Database.java | 209 ++-- .../sf/briar/db/DatabaseComponentImpl.java | 503 ++++---- .../src/net/sf/briar/db/JdbcDatabase.java | 1042 ++++++++--------- .../sf/briar/invitation/AliceConnector.java | 14 +- .../net/sf/briar/invitation/BobConnector.java | 14 +- .../net/sf/briar/invitation/Connector.java | 16 +- .../sf/briar/invitation/ConnectorGroup.java | 17 +- .../invitation/InvitationTaskFactoryImpl.java | 13 +- .../sf/briar/messaging/AuthorFactoryImpl.java | 20 +- .../sf/briar/messaging/GroupFactoryImpl.java | 22 +- .../net/sf/briar/messaging/GroupReader.java | 3 +- .../briar/messaging/MessageFactoryImpl.java | 11 +- .../net/sf/briar/messaging/MessageReader.java | 24 +- .../briar/messaging/MessageVerifierImpl.java | 13 +- .../sf/briar/messaging/PacketWriterImpl.java | 1 + .../messaging/SubscriptionUpdateReader.java | 8 +- .../messaging/duplex/DuplexConnection.java | 11 +- .../net/sf/briar/ProtocolIntegrationTest.java | 2 +- .../sf/briar/db/DatabaseComponentTest.java | 415 ++----- .../src/net/sf/briar/db/H2DatabaseTest.java | 387 +++--- .../net/sf/briar/messaging/ConstantsTest.java | 8 +- .../SimplexMessagingIntegrationTest.java | 22 +- 61 files changed, 1600 insertions(+), 2212 deletions(-) delete mode 100644 briar-android/src/net/sf/briar/android/DescendingHeaderComparator.java delete mode 100644 briar-android/src/net/sf/briar/android/contact/ContactSpinnerAdapter.java create mode 100644 briar-android/src/net/sf/briar/android/contact/ConversationItem.java create mode 100644 briar-android/src/net/sf/briar/android/contact/ConversationItemComparator.java delete mode 100644 briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java delete mode 100644 briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java delete mode 100644 briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java create mode 100644 briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java delete mode 100644 briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 8406c17870..7dabb29c79 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -52,6 +52,7 @@ <string name="messages_title">Messages</string> <string name="no_messages">(No messages)</string> <string name="format_from">From: %1$s</string> + <string name="format_to">To: %1$s</string> <string name="new_message_title">New Message</string> <string name="from">From:</string> <string name="to">To:</string> diff --git a/briar-android/src/net/sf/briar/android/DescendingHeaderComparator.java b/briar-android/src/net/sf/briar/android/DescendingHeaderComparator.java deleted file mode 100644 index 02506d7a94..0000000000 --- a/briar-android/src/net/sf/briar/android/DescendingHeaderComparator.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.sf.briar.android; - -import java.util.Comparator; - -import net.sf.briar.api.db.MessageHeader; - -public class DescendingHeaderComparator implements Comparator<MessageHeader> { - - public static final DescendingHeaderComparator INSTANCE = - new DescendingHeaderComparator(); - - public int compare(MessageHeader a, MessageHeader b) { - // The newest message comes first - long aTime = a.getTimestamp(), bTime = b.getTimestamp(); - if(aTime > bTime) return -1; - if(aTime < bTime) return 1; - return 0; - } -} \ No newline at end of file diff --git a/briar-android/src/net/sf/briar/android/SetupActivity.java b/briar-android/src/net/sf/briar/android/SetupActivity.java index 699aa16bc0..931ef24201 100644 --- a/briar-android/src/net/sf/briar/android/SetupActivity.java +++ b/briar-android/src/net/sf/briar/android/SetupActivity.java @@ -12,7 +12,6 @@ import static android.widget.LinearLayout.VERTICAL; import static net.sf.briar.android.util.CommonLayoutParams.MATCH_MATCH; import static net.sf.briar.android.util.CommonLayoutParams.WRAP_WRAP; -import java.io.IOException; import java.util.Arrays; import java.util.concurrent.Executor; @@ -188,13 +187,8 @@ public class SetupActivity extends RoboActivity implements OnClickListener { KeyPair keyPair = crypto.generateSignatureKeyPair(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final byte[] privateKey = keyPair.getPrivate().getEncoded(); - LocalAuthor a; - try { - a = authorFactory.createLocalAuthor(nickname, publicKey, - privateKey); - } catch(IOException e) { - throw new RuntimeException(e); - } + LocalAuthor a = authorFactory.createLocalAuthor(nickname, + publicKey, privateKey); showHomeScreen(referenceManager.putReference(a, LocalAuthor.class)); } diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java index aad72c171e..14e6193999 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java @@ -24,7 +24,6 @@ import java.util.logging.Logger; import javax.inject.Inject; import net.sf.briar.R; -import net.sf.briar.android.groups.NoContactsDialog; import net.sf.briar.android.invitation.AddContactActivity; import net.sf.briar.android.util.HorizontalBorder; import net.sf.briar.android.util.HorizontalSpace; @@ -34,32 +33,29 @@ import net.sf.briar.api.ContactId; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.db.NoSuchContactException; -import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.db.event.ContactAddedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.lifecycle.LifecycleManager; import net.sf.briar.api.transport.ConnectionListener; import net.sf.briar.api.transport.ConnectionRegistry; -import roboguice.activity.RoboFragmentActivity; +import roboguice.activity.RoboActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ListView; -public class ContactListActivity extends RoboFragmentActivity -implements OnClickListener, DatabaseListener, ConnectionListener, -NoContactsDialog.Listener { +public class ContactListActivity extends RoboActivity +implements OnClickListener, DatabaseListener, ConnectionListener { private static final Logger LOG = Logger.getLogger(ContactListActivity.class.getName()); @@ -68,9 +64,7 @@ NoContactsDialog.Listener { private ContactListAdapter adapter = null; private ListView list = null; private ListLoadingProgressBar loading = null; - private ImageButton addContactButton = null, composeButton = null; - private ImageButton shareButton = null; - private NoContactsDialog noContactsDialog = null; + private ImageButton addContactButton = null, shareButton = null; // Fields that are accessed from background threads must be volatile @Inject private volatile DatabaseComponent db; @@ -113,13 +107,6 @@ NoContactsDialog.Listener { footer.addView(addContactButton); footer.addView(new HorizontalSpace(this)); - composeButton = new ImageButton(this); - composeButton.setBackgroundResource(0); - composeButton.setImageResource(R.drawable.content_new_email); - composeButton.setOnClickListener(this); - footer.addView(composeButton); - footer.addView(new HorizontalSpace(this)); - shareButton = new ImageButton(this); shareButton.setBackgroundResource(0); shareButton.setImageResource(R.drawable.social_share); @@ -129,12 +116,6 @@ NoContactsDialog.Listener { layout.addView(footer); setContentView(layout); - - FragmentManager fm = getSupportFragmentManager(); - Fragment f = fm.findFragmentByTag("NoContactsDialog"); - if(f == null) noContactsDialog = new NoContactsDialog(); - else noContactsDialog = (NoContactsDialog) f; - noContactsDialog.setListener(this); } @Override @@ -142,11 +123,11 @@ NoContactsDialog.Listener { super.onResume(); db.addListener(this); connectionRegistry.addListener(this); - loadHeaders(); + loadContacts(); } - private void loadHeaders() { - clearHeaders(); + private void loadContacts() { + clearContacts(); dbUiExecutor.execute(new Runnable() { public void run() { try { @@ -157,9 +138,9 @@ NoContactsDialog.Listener { Long lastConnected = times.get(c.getId()); if(lastConnected == null) continue; try { - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(c.getId()); - displayHeaders(c, lastConnected, headers); + Collection<MessageHeader> headers = + db.getInboxMessageHeaders(c.getId()); + displayContact(c, lastConnected, headers); } catch(NoSuchContactException e) { if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); @@ -181,7 +162,7 @@ NoContactsDialog.Listener { }); } - private void clearHeaders() { + private void clearContacts() { runOnUiThread(new Runnable() { public void run() { list.setVisibility(GONE); @@ -192,8 +173,8 @@ NoContactsDialog.Listener { }); } - private void displayHeaders(final Contact c, final long lastConnected, - final Collection<PrivateMessageHeader> headers) { + private void displayContact(final Contact c, final long lastConnected, + final Collection<MessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { list.setVisibility(VISIBLE); @@ -239,14 +220,6 @@ NoContactsDialog.Listener { public void onClick(View view) { if(view == addContactButton) { startActivity(new Intent(this, AddContactActivity.class)); - } else if(view == composeButton) { - if(adapter.isEmpty()) { - FragmentManager fm = getSupportFragmentManager(); - noContactsDialog.show(fm, "NoContactsDialog"); - } else { - startActivity(new Intent(this, - WritePrivateMessageActivity.class)); - } } else if(view == shareButton) { String apkPath = getPackageCodePath(); Intent i = new Intent(ACTION_SEND); @@ -259,28 +232,28 @@ NoContactsDialog.Listener { public void eventOccurred(DatabaseEvent e) { if(e instanceof ContactAddedEvent) { - loadHeaders(); + loadContacts(); } else if(e instanceof ContactRemovedEvent) { // Reload the conversation, expecting NoSuchContactException if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading"); - reloadHeaders(((ContactRemovedEvent) e).getContactId()); + reloadContact(((ContactRemovedEvent) e).getContactId()); + } else if(e instanceof MessageAddedEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); + reloadContact(((MessageAddedEvent) e).getContactId()); } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); - loadHeaders(); - } else if(e instanceof PrivateMessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - reloadHeaders(((PrivateMessageAddedEvent) e).getContactId()); + loadContacts(); } } - private void reloadHeaders(final ContactId c) { + private void reloadContact(final ContactId c) { dbUiExecutor.execute(new Runnable() { public void run() { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(c); + Collection<MessageHeader> headers = + db.getInboxMessageHeaders(c); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Partial load took " + duration + " ms"); @@ -301,18 +274,11 @@ NoContactsDialog.Listener { } private void updateItem(final ContactId c, - final Collection<PrivateMessageHeader> headers) { + final Collection<MessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { ContactListItem item = findItem(c); - if(item == null) return; - // Replace the item with a new item containing the new headers - adapter.remove(item); - item = new ContactListItem(item.getContact(), - item.isConnected(), item.getLastConnected(), headers); - adapter.add(item); - adapter.sort(ItemComparator.INSTANCE); - adapter.notifyDataSetChanged(); + if(item != null) item.setHeaders(headers); } }); } @@ -337,28 +303,16 @@ NoContactsDialog.Listener { private void setConnected(final ContactId c, final boolean connected) { runOnUiThread(new Runnable() { public void run() { - int count = adapter.getCount(); - for(int i = 0; i < count; i++) { - ContactListItem item = adapter.getItem(i); - if(item.getContactId().equals(c)) { - if(LOG.isLoggable(INFO)) - LOG.info("Updating connection time"); - item.setConnected(connected); - item.setLastConnected(System.currentTimeMillis()); - list.invalidateViews(); - return; - } - } + ContactListItem item = findItem(c); + if(item == null) return; + if(LOG.isLoggable(INFO)) LOG.info("Updating connection time"); + item.setConnected(connected); + item.setLastConnected(System.currentTimeMillis()); + list.invalidateViews(); } }); } - public void contactCreationSelected() { - startActivity(new Intent(this, AddContactActivity.class)); - } - - public void contactCreationCancelled() {} - private static class ItemComparator implements Comparator<ContactListItem> { private static final ItemComparator INSTANCE = new ItemComparator(); diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java index 5787c11588..f075db23af 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java @@ -1,15 +1,11 @@ package net.sf.briar.android.contact; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import net.sf.briar.android.DescendingHeaderComparator; import net.sf.briar.api.AuthorId; import net.sf.briar.api.Contact; import net.sf.briar.api.ContactId; -import net.sf.briar.api.db.PrivateMessageHeader; +import net.sf.briar.api.db.MessageHeader; // This class is not thread-safe class ContactListItem { @@ -17,27 +13,27 @@ class ContactListItem { private final Contact contact; private boolean connected; private long lastConnected; - private final boolean empty; - private final long timestamp; - private final int unread; + private boolean empty; + private long timestamp; + private int unread; ContactListItem(Contact contact, boolean connected, long lastConnected, - Collection<PrivateMessageHeader> headers) { + Collection<MessageHeader> headers) { this.contact = contact; this.connected = connected; this.lastConnected = lastConnected; + setHeaders(headers); + } + + void setHeaders(Collection<MessageHeader> headers) { empty = headers.isEmpty(); - if(empty) { - timestamp = 0; - unread = 0; - } else { - List<PrivateMessageHeader> list = - new ArrayList<PrivateMessageHeader>(headers); - Collections.sort(list, DescendingHeaderComparator.INSTANCE); - timestamp = list.get(0).getTimestamp(); - int unread = 0; - for(PrivateMessageHeader h : list) if(!h.isRead()) unread++; - this.unread = unread; + timestamp = 0; + unread = 0; + if(!empty) { + for(MessageHeader h : headers) { + if(h.getTimestamp() > timestamp) timestamp = h.getTimestamp(); + if(!h.isRead()) unread++; + } } } diff --git a/briar-android/src/net/sf/briar/android/contact/ContactSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/contact/ContactSpinnerAdapter.java deleted file mode 100644 index 58e17c02d2..0000000000 --- a/briar-android/src/net/sf/briar/android/contact/ContactSpinnerAdapter.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.briar.android.contact; - -import static net.sf.briar.android.contact.ContactItem.NEW; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import net.sf.briar.R; -import android.content.Context; -import android.content.res.Resources; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.SpinnerAdapter; -import android.widget.TextView; - -public class ContactSpinnerAdapter extends BaseAdapter -implements SpinnerAdapter { - - private final Context ctx; - private final List<ContactItem> list = new ArrayList<ContactItem>(); - - public ContactSpinnerAdapter(Context ctx) { - this.ctx = ctx; - } - - public void add(ContactItem item) { - list.add(item); - } - - public void clear() { - list.clear(); - } - - public int getCount() { - return list.isEmpty() ? 0 : list.size() + 1; - } - - @Override - public View getDropDownView(int position, View convertView, - ViewGroup parent) { - return getView(position, convertView, parent); - } - - public ContactItem getItem(int position) { - if(position == list.size()) return NEW; - return list.get(position); - } - - public long getItemId(int position) { - return android.R.layout.simple_spinner_item; - } - - public View getView(int position, View convertView, ViewGroup parent) { - TextView name = new TextView(ctx); - name.setTextSize(18); - name.setMaxLines(1); - Resources res = ctx.getResources(); - int pad = res.getInteger(R.integer.spinner_padding); - name.setPadding(pad, pad, pad, pad); - ContactItem item = getItem(position); - if(item == NEW) name.setText(R.string.new_contact_item); - else name.setText(item.getContact().getAuthor().getName()); - return name; - } - - @Override - public boolean isEmpty() { - return list.isEmpty(); - } - - public void sort(Comparator<ContactItem> comparator) { - Collections.sort(list, comparator); - } -} diff --git a/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java b/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java index a0fe1020c1..30a5502a9d 100644 --- a/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java @@ -16,22 +16,21 @@ import java.util.logging.Logger; import javax.inject.Inject; import net.sf.briar.R; -import net.sf.briar.android.AscendingHeaderComparator; import net.sf.briar.android.util.HorizontalBorder; import net.sf.briar.android.util.ListLoadingProgressBar; -import net.sf.briar.api.AuthorId; import net.sf.briar.api.ContactId; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.db.NoSuchContactException; -import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.lifecycle.LifecycleManager; +import net.sf.briar.api.messaging.GroupId; import roboguice.activity.RoboActivity; import android.content.Intent; import android.os.Bundle; @@ -53,13 +52,14 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { private ConversationAdapter adapter = null; private ListView list = null; private ListLoadingProgressBar loading = null; + private ImageButton composeButton = null; // Fields that are accessed from background threads must be volatile @Inject private volatile DatabaseComponent db; @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor; @Inject private volatile LifecycleManager lifecycleManager; private volatile ContactId contactId = null; - private volatile AuthorId localAuthorId = null; + private volatile GroupId groupId = null; @Override public void onCreate(Bundle state) { @@ -72,9 +72,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME"); if(contactName == null) throw new IllegalStateException(); setTitle(contactName); - byte[] b = i.getByteArrayExtra("net.sf.briar.LOCAL_AUTHOR_ID"); - if(b == null) throw new IllegalStateException(); - localAuthorId = new AuthorId(b); LinearLayout layout = new LinearLayout(this); layout.setLayoutParams(MATCH_MATCH); @@ -96,9 +93,10 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { layout.addView(new HorizontalBorder(this)); - ImageButton composeButton = new ImageButton(this); + composeButton = new ImageButton(this); composeButton.setBackgroundResource(0); composeButton.setImageResource(R.drawable.content_new_email); + composeButton.setEnabled(false); // Enabled after loading the headers composeButton.setOnClickListener(this); layout.addView(composeButton); @@ -118,8 +116,9 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(contactId); + groupId = db.getInboxGroup(contactId); + Collection<MessageHeader> headers = + db.getInboxMessageHeaders(contactId); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); @@ -143,15 +142,16 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { }); } - private void displayHeaders( - final Collection<PrivateMessageHeader> headers) { + private void displayHeaders(final Collection<MessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { list.setVisibility(VISIBLE); loading.setVisibility(GONE); + composeButton.setEnabled(true); adapter.clear(); - for(PrivateMessageHeader h : headers) adapter.add(h); - adapter.sort(AscendingHeaderComparator.INSTANCE); + for(MessageHeader h : headers) + adapter.add(new ConversationItem(h)); + adapter.sort(ConversationItemComparator.INSTANCE); adapter.notifyDataSetChanged(); selectFirstUnread(); } @@ -161,7 +161,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { private void selectFirstUnread() { int firstUnread = -1, count = adapter.getCount(); for(int i = 0; i < count; i++) { - if(!adapter.getItem(i).isRead()) { + if(!adapter.getItem(i).getHeader().isRead()) { firstUnread = i; break; } @@ -200,22 +200,21 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { } }); } - } else if(e instanceof MessageExpiredEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); - loadHeaders(); - } else if(e instanceof PrivateMessageAddedEvent) { - PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e; - if(p.getContactId().equals(contactId)) { + } else if(e instanceof MessageAddedEvent) { + if(((MessageAddedEvent) e).getContactId().equals(contactId)) { if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); loadHeaders(); } + } else if(e instanceof MessageExpiredEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); + loadHeaders(); } } public void onClick(View view) { Intent i = new Intent(this, WritePrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); - i.putExtra("net.sf.briar.LOCAL_AUTHOR_ID", localAuthorId.getBytes()); + i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); startActivity(i); } @@ -225,14 +224,15 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { } private void displayMessage(int position) { - PrivateMessageHeader item = adapter.getItem(position); + MessageHeader header = adapter.getItem(position).getHeader(); Intent i = new Intent(this, ReadPrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); i.putExtra("net.sf.briar.CONTACT_NAME", contactName); - i.putExtra("net.sf.briar.AUTHOR_NAME", item.getAuthor().getName()); - i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes()); - i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType()); - i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp()); + i.putExtra("net.sf.briar.GROUP_ID", header.getGroupId().getBytes()); + i.putExtra("net.sf.briar.AUTHOR_NAME", header.getAuthor().getName()); + i.putExtra("net.sf.briar.MESSAGE_ID", header.getId().getBytes()); + i.putExtra("net.sf.briar.CONTENT_TYPE", header.getContentType()); + i.putExtra("net.sf.briar.TIMESTAMP", header.getTimestamp()); startActivityForResult(i, position); } } diff --git a/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java index 3497efd103..d1a4d7d209 100644 --- a/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java +++ b/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java @@ -7,7 +7,7 @@ import static net.sf.briar.android.util.CommonLayoutParams.WRAP_WRAP_1; import java.util.ArrayList; import net.sf.briar.R; -import net.sf.briar.api.db.PrivateMessageHeader; +import net.sf.briar.api.db.MessageHeader; import android.content.Context; import android.content.res.Resources; import android.text.format.DateUtils; @@ -17,21 +17,21 @@ import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.TextView; -class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> { +class ConversationAdapter extends ArrayAdapter<ConversationItem> { ConversationAdapter(Context ctx) { super(ctx, android.R.layout.simple_expandable_list_item_1, - new ArrayList<PrivateMessageHeader>()); + new ArrayList<ConversationItem>()); } @Override public View getView(int position, View convertView, ViewGroup parent) { - PrivateMessageHeader item = getItem(position); + MessageHeader header = getItem(position).getHeader(); Context ctx = getContext(); LinearLayout layout = new LinearLayout(ctx); layout.setOrientation(HORIZONTAL); - if(!item.isRead()) { + if(!header.isRead()) { Resources res = ctx.getResources(); layout.setBackgroundColor(res.getColor(R.color.unread_background)); } @@ -42,13 +42,13 @@ class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> { name.setTextSize(18); name.setMaxLines(1); name.setPadding(10, 10, 10, 10); - name.setText(item.getAuthor().getName()); + name.setText(header.getAuthor().getName()); layout.addView(name); TextView date = new TextView(ctx); date.setTextSize(14); date.setPadding(0, 10, 10, 10); - long then = item.getTimestamp(), now = System.currentTimeMillis(); + long then = header.getTimestamp(), now = System.currentTimeMillis(); date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT)); layout.addView(date); diff --git a/briar-android/src/net/sf/briar/android/contact/ConversationItem.java b/briar-android/src/net/sf/briar/android/contact/ConversationItem.java new file mode 100644 index 0000000000..a8e777eba1 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/contact/ConversationItem.java @@ -0,0 +1,37 @@ +package net.sf.briar.android.contact; + +import net.sf.briar.api.db.MessageHeader; + +// This class is not thread-safe +class ConversationItem { + + private final MessageHeader header; + private boolean expanded; + private byte[] body; + + ConversationItem(MessageHeader header) { + this.header = header; + expanded = false; + body = null; + } + + MessageHeader getHeader() { + return header; + } + + boolean isExpanded() { + return expanded; + } + + void setExpanded(boolean expanded) { + this.expanded = expanded; + } + + byte[] getBody() { + return body; + } + + void setBody(byte[] body) { + this.body = body; + } +} diff --git a/briar-android/src/net/sf/briar/android/contact/ConversationItemComparator.java b/briar-android/src/net/sf/briar/android/contact/ConversationItemComparator.java new file mode 100644 index 0000000000..bdeca9fb55 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/contact/ConversationItemComparator.java @@ -0,0 +1,19 @@ +package net.sf.briar.android.contact; + +import java.util.Comparator; + +public class ConversationItemComparator +implements Comparator<ConversationItem> { + + public static final ConversationItemComparator INSTANCE = + new ConversationItemComparator(); + + public int compare(ConversationItem a, ConversationItem b) { + // The oldest message comes first + long aTime = a.getHeader().getTimestamp(); + long bTime = b.getHeader().getTimestamp(); + if(aTime < bTime) return -1; + if(aTime > bTime) return 1; + return 0; + } +} diff --git a/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java index 64927e0c69..6c970f4bbf 100644 --- a/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java @@ -26,6 +26,7 @@ import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.NoSuchMessageException; import net.sf.briar.api.lifecycle.LifecycleManager; +import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.MessageId; import roboguice.activity.RoboActivity; import android.content.Intent; @@ -60,6 +61,7 @@ implements OnClickListener { @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor; @Inject private volatile LifecycleManager lifecycleManager; private volatile MessageId messageId = null; + private volatile GroupId groupId = null; private volatile long timestamp = -1; @Override @@ -78,6 +80,9 @@ implements OnClickListener { byte[] b = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID"); if(b == null) throw new IllegalStateException(); messageId = new MessageId(b); + b = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); + if(b == null) throw new IllegalStateException(); + groupId = new GroupId(b); String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE"); if(contentType == null) throw new IllegalStateException(); timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1); @@ -262,6 +267,7 @@ implements OnClickListener { } else if(view == replyButton) { Intent i = new Intent(this, WritePrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); + i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes()); i.putExtra("net.sf.briar.TIMESTAMP", timestamp); startActivity(i); diff --git a/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java index 38891115c5..a0b110c03b 100644 --- a/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java @@ -12,49 +12,46 @@ import static net.sf.briar.android.util.CommonLayoutParams.MATCH_WRAP; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; -import java.util.Collection; import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.inject.Inject; import net.sf.briar.R; -import net.sf.briar.android.invitation.AddContactActivity; import net.sf.briar.android.util.HorizontalSpace; -import net.sf.briar.api.AuthorId; import net.sf.briar.api.Contact; import net.sf.briar.api.ContactId; import net.sf.briar.api.LocalAuthor; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NoSuchContactException; +import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.lifecycle.LifecycleManager; +import net.sf.briar.api.messaging.Group; +import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageFactory; import net.sf.briar.api.messaging.MessageId; import roboguice.activity.RoboActivity; import android.content.Intent; +import android.content.res.Resources; import android.os.Bundle; import android.text.InputType; import android.view.View; import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; -import android.widget.Spinner; import android.widget.TextView; public class WritePrivateMessageActivity extends RoboActivity -implements OnItemSelectedListener, OnClickListener { +implements OnClickListener { private static final Logger LOG = Logger.getLogger(WritePrivateMessageActivity.class.getName()); - private TextView from = null; - private ContactSpinnerAdapter adapter = null; - private Spinner spinner = null; + private TextView from = null, to = null; private ImageButton sendButton = null; private EditText content = null; @@ -63,10 +60,13 @@ implements OnItemSelectedListener, OnClickListener { @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor; @Inject private volatile LifecycleManager lifecycleManager; @Inject private volatile MessageFactory messageFactory; - private volatile LocalAuthor localAuthor = null; private volatile ContactId contactId = null; + private volatile GroupId groupId = null; private volatile MessageId parentId = null; private volatile long timestamp = -1; + private volatile Contact contact = null; + private volatile LocalAuthor localAuthor = null; + private volatile Group group = null; @Override public void onCreate(Bundle state) { @@ -74,16 +74,15 @@ implements OnItemSelectedListener, OnClickListener { Intent i = getIntent(); int id = i.getIntExtra("net.sf.briar.CONTACT_ID", -1); - if(id != -1) contactId = new ContactId(id); - byte[] b = i.getByteArrayExtra("net.sf.briar.PARENT_ID"); + if(id == -1) throw new IllegalStateException(); + contactId = new ContactId(id); + byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); + if(b == null) throw new IllegalStateException(); + groupId = new GroupId(b); + b = i.getByteArrayExtra("net.sf.briar.PARENT_ID"); if(b != null) parentId = new MessageId(b); timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1); - if(state != null) { - id = state.getInt("net.sf.briar.CONTACT_ID", -1); - if(id != -1) contactId = new ContactId(id); - } - LinearLayout layout = new LinearLayout(this); layout.setLayoutParams(MATCH_WRAP); layout.setOrientation(VERTICAL); @@ -104,28 +103,16 @@ implements OnItemSelectedListener, OnClickListener { sendButton = new ImageButton(this); sendButton.setBackgroundResource(0); sendButton.setImageResource(R.drawable.social_send_now); - sendButton.setEnabled(false); // Enabled after loading the local author + sendButton.setEnabled(false); // Enabled after loading the group sendButton.setOnClickListener(this); header.addView(sendButton); layout.addView(header); - header = new LinearLayout(this); - header.setLayoutParams(MATCH_WRAP); - header.setOrientation(HORIZONTAL); - header.setGravity(CENTER_VERTICAL); - - TextView to = new TextView(this); + to = new TextView(this); to.setTextSize(18); - to.setPadding(10, 0, 0, 10); + to.setPadding(10, 10, 10, 10); to.setText(R.string.to); - header.addView(to); - - adapter = new ContactSpinnerAdapter(this); - spinner = new Spinner(this); - spinner.setAdapter(adapter); - spinner.setOnItemSelectedListener(this); - header.addView(spinner); - layout.addView(header); + layout.addView(to); content = new EditText(this); content.setId(1); @@ -140,20 +127,26 @@ implements OnItemSelectedListener, OnClickListener { @Override public void onResume() { super.onResume(); - loadContacts(); + loadAuthorsAndGroup(); } - private void loadContacts() { + private void loadAuthorsAndGroup() { dbUiExecutor.execute(new Runnable() { public void run() { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<Contact> contacts = db.getContacts(); + contact = db.getContact(contactId); + localAuthor = db.getLocalAuthor(contact.getLocalAuthorId()); + group = db.getGroup(groupId); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) - LOG.info("Loading contacts took " + duration + " ms"); - displayContacts(contacts); + LOG.info("Load took " + duration + " ms"); + displayAuthors(); + } catch(NoSuchContactException e) { + finish(); + } catch(NoSuchSubscriptionException e) { + finish(); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -165,101 +158,33 @@ implements OnItemSelectedListener, OnClickListener { }); } - private void displayContacts(final Collection<Contact> contacts) { + private void displayAuthors() { runOnUiThread(new Runnable() { public void run() { - if(contacts.isEmpty()) finish(); - adapter.clear(); - for(Contact c : contacts) adapter.add(new ContactItem(c)); - adapter.sort(ContactItemComparator.INSTANCE); - adapter.notifyDataSetChanged(); - int count = adapter.getCount(); - for(int i = 0; i < count; i++) { - ContactItem item = adapter.getItem(i); - if(item == ContactItem.NEW) continue; - if(item.getContact().getId().equals(contactId)) { - spinner.setSelection(i); - break; - } - } - } - }); - } - - @Override - public void onSaveInstanceState(Bundle state) { - super.onSaveInstanceState(state); - if(contactId != null) - state.putInt("net.sf.briar.CONTACT_ID", contactId.getInt()); - } - - public void onItemSelected(AdapterView<?> parent, View view, int position, - long id) { - ContactItem item = adapter.getItem(position); - if(item == ContactItem.NEW) { - contactId = null; - localAuthor = null; - startActivity(new Intent(this, AddContactActivity.class)); - } else { - Contact c = item.getContact(); - contactId = c.getId(); - localAuthor = null; - loadLocalAuthor(c.getLocalAuthorId()); - } - sendButton.setEnabled(false); - } - - private void loadLocalAuthor(final AuthorId a) { - dbUiExecutor.execute(new Runnable() { - public void run() { - try { - lifecycleManager.waitForDatabase(); - long now = System.currentTimeMillis(); - localAuthor = db.getLocalAuthor(a); - long duration = System.currentTimeMillis() - now; - if(LOG.isLoggable(INFO)) - LOG.info("Loading author took " + duration + " ms"); - displayLocalAuthor(); - } catch(DbException e) { - if(LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } catch(InterruptedException e) { - LOG.info("Interrupted while waiting for database"); - Thread.currentThread().interrupt(); - } - } - }); - } - - private void displayLocalAuthor() { - runOnUiThread(new Runnable() { - public void run() { - String format = getResources().getString(R.string.format_from); - from.setText(String.format(format, localAuthor.getName())); + Resources res = getResources(); + String format = res.getString(R.string.format_from); + String name = localAuthor.getName(); + from.setText(String.format(format, name)); + format = res.getString(R.string.format_to); + name = contact.getAuthor().getName(); + to.setText(String.format(format, name)); sendButton.setEnabled(true); } }); } - public void onNothingSelected(AdapterView<?> parent) { - contactId = null; - sendButton.setEnabled(false); - } - public void onClick(View view) { - if(localAuthor == null || contactId == null) + if(contact == null || localAuthor == null) throw new IllegalStateException(); try { - byte[] b = content.getText().toString().getBytes("UTF-8"); - storeMessage(localAuthor, contactId, b); + storeMessage(content.getText().toString().getBytes("UTF-8")); } catch(UnsupportedEncodingException e) { throw new RuntimeException(e); } finish(); } - private void storeMessage(final LocalAuthor localAuthor, - final ContactId contactId, final byte[] body) { + private void storeMessage(final byte[] body) { dbUiExecutor.execute(new Runnable() { public void run() { try { @@ -267,10 +192,10 @@ implements OnItemSelectedListener, OnClickListener { // Don't use an earlier timestamp than the parent long time = System.currentTimeMillis(); time = Math.max(time, timestamp + 1); - Message m = messageFactory.createPrivateMessage(parentId, - "text/plain", time, body); + Message m = messageFactory.createAnonymousMessage(parentId, + group, "text/plain", time, body); long now = System.currentTimeMillis(); - db.addLocalPrivateMessage(m, contactId); + db.addLocalMessage(m); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Storing message took " + duration + " ms"); diff --git a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java index 6241ba2f37..c96a1de59b 100644 --- a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java @@ -77,7 +77,7 @@ SelectContactsDialog.Listener { setTitle(name); b = i.getByteArrayExtra("net.sf.briar.GROUP_SALT"); if(b == null) throw new IllegalStateException(); - group = new Group(id, name, b); + group = new Group(id, name, b, false); subscribed = i.getBooleanExtra("net.sf.briar.SUBSCRIBED", false); boolean all = i.getBooleanExtra("net.sf.briar.VISIBLE_TO_ALL", false); @@ -207,11 +207,11 @@ SelectContactsDialog.Listener { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); if(subscribe) { - if(!wasSubscribed) db.subscribe(group); + if(!wasSubscribed) db.addGroup(group); db.setVisibleToAll(group.getId(), all); if(!all) db.setVisibility(group.getId(), visible); } else if(wasSubscribed) { - db.unsubscribe(group); + db.removeGroup(group); } long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) diff --git a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java index dd7bb39812..08c3d948c2 100644 --- a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java @@ -13,7 +13,6 @@ import static java.util.logging.Level.WARNING; import static net.sf.briar.android.util.CommonLayoutParams.MATCH_MATCH; import static net.sf.briar.android.util.CommonLayoutParams.WRAP_WRAP; -import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.concurrent.Executor; @@ -174,9 +173,9 @@ SelectContactsDialog.Listener { public void run() { try { lifecycleManager.waitForDatabase(); - Group g = groupFactory.createGroup(name); + Group g = groupFactory.createGroup(name, false); long now = System.currentTimeMillis(); - db.subscribe(g); + db.addGroup(g); if(all) db.setVisibleToAll(g.getId(), true); else db.setVisibility(g.getId(), visible); long duration = System.currentTimeMillis() - now; @@ -189,8 +188,6 @@ SelectContactsDialog.Listener { if(LOG.isLoggable(INFO)) LOG.info("Interrupted while waiting for database"); Thread.currentThread().interrupt(); - } catch(IOException e) { - throw new RuntimeException(e); } runOnUiThread(new Runnable() { public void run() { diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java index 965137b0f5..fef29b665a 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java @@ -25,11 +25,11 @@ import net.sf.briar.api.Author; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.GroupMessageHeader; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.GroupMessageAddedEvent; +import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.lifecycle.LifecycleManager; @@ -116,8 +116,8 @@ OnClickListener, OnItemClickListener { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(groupId); + Collection<MessageHeader> headers = + db.getMessageHeaders(groupId); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); @@ -141,13 +141,13 @@ OnClickListener, OnItemClickListener { }); } - private void displayHeaders(final Collection<GroupMessageHeader> headers) { + private void displayHeaders(final Collection<MessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { list.setVisibility(VISIBLE); loading.setVisibility(GONE); adapter.clear(); - for(GroupMessageHeader h : headers) adapter.add(h); + for(MessageHeader h : headers) adapter.add(h); adapter.sort(AscendingHeaderComparator.INSTANCE); adapter.notifyDataSetChanged(); selectFirstUnread(); @@ -187,9 +187,8 @@ OnClickListener, OnItemClickListener { } public void eventOccurred(DatabaseEvent e) { - if(e instanceof GroupMessageAddedEvent) { - GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; - if(g.getGroup().getId().equals(groupId)) { + if(e instanceof MessageAddedEvent) { + if(((MessageAddedEvent) e).getGroup().getId().equals(groupId)) { if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); loadHeaders(); } @@ -221,7 +220,7 @@ OnClickListener, OnItemClickListener { } private void displayMessage(int position) { - GroupMessageHeader item = adapter.getItem(position); + MessageHeader item = adapter.getItem(position); Intent i = new Intent(this, ReadGroupPostActivity.class); i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); i.putExtra("net.sf.briar.GROUP_NAME", groupName); diff --git a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java index c85f452968..ac4241e9c6 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java @@ -8,7 +8,7 @@ import java.util.ArrayList; import net.sf.briar.R; import net.sf.briar.api.Author; -import net.sf.briar.api.db.GroupMessageHeader; +import net.sf.briar.api.db.MessageHeader; import android.content.Context; import android.content.res.Resources; import android.text.format.DateUtils; @@ -18,16 +18,16 @@ import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.TextView; -class GroupAdapter extends ArrayAdapter<GroupMessageHeader> { +class GroupAdapter extends ArrayAdapter<MessageHeader> { GroupAdapter(Context ctx) { super(ctx, android.R.layout.simple_expandable_list_item_1, - new ArrayList<GroupMessageHeader>()); + new ArrayList<MessageHeader>()); } @Override public View getView(int position, View convertView, ViewGroup parent) { - GroupMessageHeader item = getItem(position); + MessageHeader item = getItem(position); Context ctx = getContext(); Resources res = ctx.getResources(); diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java index 7b6d624e35..485685715d 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java @@ -27,11 +27,11 @@ import net.sf.briar.android.util.ListLoadingProgressBar; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.GroupMessageHeader; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.GroupMessageAddedEvent; +import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.SubscriptionAddedEvent; @@ -140,10 +140,11 @@ OnItemClickListener { long now = System.currentTimeMillis(); for(GroupStatus s : db.getAvailableGroups()) { Group g = s.getGroup(); + if(g.isPrivate()) continue; if(s.isSubscribed()) { try { - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(g.getId()); + Collection<MessageHeader> headers = + db.getMessageHeaders(g.getId()); displayHeaders(g, headers); } catch(NoSuchSubscriptionException e) { if(LOG.isLoggable(INFO)) @@ -181,7 +182,7 @@ OnItemClickListener { } private void displayHeaders(final Group g, - final Collection<GroupMessageHeader> headers) { + final Collection<MessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { list.setVisibility(VISIBLE); @@ -240,9 +241,12 @@ OnItemClickListener { } public void eventOccurred(DatabaseEvent e) { - if(e instanceof GroupMessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - loadHeaders(((GroupMessageAddedEvent) e).getGroup()); + if(e instanceof MessageAddedEvent) { + Group g = ((MessageAddedEvent) e).getGroup(); + if(!g.isPrivate()) { + if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); + loadHeaders(g); + } } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); loadHeaders(); @@ -254,9 +258,12 @@ OnItemClickListener { if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); loadHeaders(); } else if(e instanceof SubscriptionRemovedEvent) { - // Reload the group, expecting NoSuchSubscriptionException - if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); - loadHeaders(((SubscriptionRemovedEvent) e).getGroup()); + Group g = ((SubscriptionRemovedEvent) e).getGroup(); + if(!g.isPrivate()) { + // Reload the group, expecting NoSuchSubscriptionException + if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); + loadHeaders(g); + } } } @@ -266,8 +273,8 @@ OnItemClickListener { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(g.getId()); + Collection<MessageHeader> headers = + db.getMessageHeaders(g.getId()); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Partial load took " + duration + " ms"); @@ -307,8 +314,10 @@ OnItemClickListener { lifecycleManager.waitForDatabase(); int available = 0; long now = System.currentTimeMillis(); - for(GroupStatus s : db.getAvailableGroups()) - if(!s.isSubscribed()) available++; + for(GroupStatus s : db.getAvailableGroups()) { + if(!s.isSubscribed() && !s.getGroup().isPrivate()) + available++; + } long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Loading available took " + duration + " ms"); @@ -354,9 +363,9 @@ OnItemClickListener { startActivity(new Intent(this, ManageGroupsActivity.class)); } else { Intent i = new Intent(this, GroupActivity.class); - i.putExtra("net.sf.briar.GROUP_ID", - item.getGroup().getId().getBytes()); - i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName()); + Group g = item.getGroup(); + i.putExtra("net.sf.briar.GROUP_ID", g.getId().getBytes()); + i.putExtra("net.sf.briar.GROUP_NAME", g.getName()); startActivity(i); } } diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java index cf8cd18c82..d82bb2feec 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java @@ -1,19 +1,16 @@ package net.sf.briar.android.groups; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; -import net.sf.briar.android.DescendingHeaderComparator; import net.sf.briar.api.Author; -import net.sf.briar.api.db.GroupMessageHeader; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.messaging.Group; class GroupListItem { static final GroupListItem MANAGE = new GroupListItem(null, - Collections.<GroupMessageHeader>emptyList()); + Collections.<MessageHeader>emptyList()); private final Group group; private final boolean empty; @@ -21,7 +18,7 @@ class GroupListItem { private final long timestamp; private final int unread; - GroupListItem(Group group, Collection<GroupMessageHeader> headers) { + GroupListItem(Group group, Collection<MessageHeader> headers) { this.group = group; empty = headers.isEmpty(); if(empty) { @@ -30,17 +27,21 @@ class GroupListItem { timestamp = 0; unread = 0; } else { - List<GroupMessageHeader> list = - new ArrayList<GroupMessageHeader>(headers); - Collections.sort(list, DescendingHeaderComparator.INSTANCE); - GroupMessageHeader newest = list.get(0); + MessageHeader newest = null; + long timestamp = 0; + int unread = 0; + for(MessageHeader h : headers) { + if(h.getTimestamp() > timestamp) { + timestamp = h.getTimestamp(); + newest = h; + } + if(!h.isRead()) unread++; + } Author a = newest.getAuthor(); if(a == null) authorName = null; else authorName = a.getName(); contentType = newest.getContentType(); - timestamp = newest.getTimestamp(); - int unread = 0; - for(GroupMessageHeader h : list) if(!h.isRead()) unread++; + this.timestamp = newest.getTimestamp(); this.unread = unread; } } diff --git a/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java b/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java index 14d326c9c0..a9be14f8fd 100644 --- a/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java @@ -5,6 +5,7 @@ import static java.util.logging.Level.WARNING; import static net.sf.briar.android.groups.ManageGroupsItem.NONE; import static net.sf.briar.android.util.CommonLayoutParams.MATCH_MATCH; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.concurrent.Executor; @@ -66,20 +67,23 @@ implements DatabaseListener, OnItemClickListener { public void onResume() { super.onResume(); db.addListener(this); - loadAvailableGroups(); + loadGroups(); } - private void loadAvailableGroups() { + private void loadGroups() { dbUiExecutor.execute(new Runnable() { public void run() { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<GroupStatus> available = db.getAvailableGroups(); + Collection<GroupStatus> available = + new ArrayList<GroupStatus>(); + for(GroupStatus s : db.getAvailableGroups()) + if(!s.getGroup().isPrivate()) available.add(s); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); - displayAvailableGroups(available); + displayGroups(available); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -92,14 +96,13 @@ implements DatabaseListener, OnItemClickListener { }); } - private void displayAvailableGroups( - final Collection<GroupStatus> available) { + private void displayGroups(final Collection<GroupStatus> available) { runOnUiThread(new Runnable() { public void run() { setContentView(list); adapter.clear(); - for(GroupStatus g : available) - adapter.add(new ManageGroupsItem(g)); + for(GroupStatus s : available) + adapter.add(new ManageGroupsItem(s)); adapter.sort(ItemComparator.INSTANCE); adapter.notifyDataSetChanged(); } @@ -116,13 +119,13 @@ implements DatabaseListener, OnItemClickListener { if(e instanceof RemoteSubscriptionsUpdatedEvent) { if(LOG.isLoggable(INFO)) LOG.info("Remote subscriptions changed, reloading"); - loadAvailableGroups(); + loadGroups(); } else if(e instanceof SubscriptionAddedEvent) { if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); - loadAvailableGroups(); + loadGroups(); } else if(e instanceof SubscriptionRemovedEvent) { if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); - loadAvailableGroups(); + loadGroups(); } } diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java index 4ea20fef52..fc79780222 100644 --- a/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java @@ -11,6 +11,7 @@ import static net.sf.briar.android.util.CommonLayoutParams.MATCH_WRAP; import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -71,10 +72,10 @@ implements OnItemSelectedListener, OnClickListener { @Inject private volatile DatabaseComponent db; @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor; @Inject private volatile LifecycleManager lifecycleManager; - private volatile LocalAuthor localAuthor = null; - private volatile Group group = null; private volatile MessageId parentId = null; private volatile long timestamp = -1; + private volatile LocalAuthor localAuthor = null; + private volatile Group group = null; @Override public void onCreate(Bundle state) { @@ -213,7 +214,9 @@ implements OnItemSelectedListener, OnClickListener { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<Group> groups = db.getSubscriptions(); + Collection<Group> groups = new ArrayList<Group>(); + for(Group g : db.getGroups()) + if(!g.isPrivate()) groups.add(g); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Loading groups took " + duration + " ms"); @@ -341,7 +344,7 @@ implements OnItemSelectedListener, OnClickListener { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - db.addLocalGroupMessage(m); + db.addLocalMessage(m); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Storing message took " + duration + " ms"); diff --git a/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java b/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java index bd4feb7c04..2010c551ef 100644 --- a/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java +++ b/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java @@ -13,7 +13,6 @@ import static java.util.logging.Level.WARNING; import static net.sf.briar.android.util.CommonLayoutParams.MATCH_MATCH; import static net.sf.briar.android.util.CommonLayoutParams.WRAP_WRAP; -import java.io.IOException; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -123,13 +122,8 @@ implements OnEditorActionListener, OnClickListener { KeyPair keyPair = crypto.generateSignatureKeyPair(); final byte[] publicKey = keyPair.getPublic().getEncoded(); final byte[] privateKey = keyPair.getPrivate().getEncoded(); - LocalAuthor a; - try { - a = authorFactory.createLocalAuthor(nickname, publicKey, - privateKey); - } catch(IOException e) { - throw new RuntimeException(e); - } + LocalAuthor a = authorFactory.createLocalAuthor(nickname, + publicKey, privateKey); storeLocalAuthor(a); } }); diff --git a/briar-api/src/net/sf/briar/api/AuthorFactory.java b/briar-api/src/net/sf/briar/api/AuthorFactory.java index 5b881f9f5b..dfb808a15e 100644 --- a/briar-api/src/net/sf/briar/api/AuthorFactory.java +++ b/briar-api/src/net/sf/briar/api/AuthorFactory.java @@ -1,11 +1,9 @@ package net.sf.briar.api; -import java.io.IOException; - public interface AuthorFactory { - Author createAuthor(String name, byte[] publicKey) throws IOException; + Author createAuthor(String name, byte[] publicKey); LocalAuthor createLocalAuthor(String name, byte[] publicKey, - byte[] privateKey) throws IOException; + byte[] privateKey); } diff --git a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java index baee643e92..a3bf48e15c 100644 --- a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java +++ b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java @@ -47,6 +47,9 @@ public interface CryptoComponent { byte[] deriveMasterSecret(byte[] theirPublicKey, KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException; + /** Derives a group salt from the given master secret. */ + byte[] deriveGroupSalt(byte[] secret); + /** * Derives an initial secret for the given transport from the given master * secret. diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index 4e438d2781..53dbba1cde 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -48,22 +48,25 @@ public interface DatabaseComponent { void removeListener(DatabaseListener d); /** - * Stores a contact with the given pseudonym, associated with the given - * local pseudonym, and returns an ID for the contact. + * Stores a contact associated with the given local and remote pseudonyms, + * and returns an ID for the contact. */ ContactId addContact(Author remote, AuthorId local) throws DbException; /** Stores an endpoint. */ void addEndpoint(Endpoint ep) throws DbException; + /** + * Subscribes to a group, or returns false if the user already has the + * maximum number of public subscriptions. + */ + boolean addGroup(Group g) throws DbException; + /** Stores a local pseudonym. */ void addLocalAuthor(LocalAuthor a) throws DbException; - /** Stores a locally generated group message. */ - void addLocalGroupMessage(Message m) throws DbException; - - /** Stores a locally generated private message. */ - void addLocalPrivateMessage(Message m, ContactId c) throws DbException; + /** Stores a local message. */ + void addLocalMessage(Message m) throws DbException; /** * Stores the given temporary secrets and deletes any secrets that have @@ -77,14 +80,17 @@ public interface DatabaseComponent { */ boolean addTransport(TransportId t, long maxLatency) throws DbException; + /** Returns true if any messages are sendable to the given contact. */ + boolean containsSendableMessages(ContactId c) throws DbException; + /** - * Generates an acknowledgement for the given contact, or returns null if - * there are no messages to acknowledge. + * Returns an acknowledgement for the given contact, or null if there are + * no messages to acknowledge. */ Ack generateAck(ContactId c, int maxMessages) throws DbException; /** - * Generates a batch of raw messages for the given contact, with a total + * Returns a batch of raw messages for the given contact, with a total * length less than or equal to the given length, for transmission over a * transport with the given maximum latency. Returns null if there are no * sendable messages that fit in the given length. @@ -93,7 +99,7 @@ public interface DatabaseComponent { long maxLatency) throws DbException; /** - * Generates a batch of raw messages for the given contact from the given + * Returns a batch of raw messages for the given contact from the given * collection of requested messages, with a total length less than or equal * to the given length, for transmission over a transport with the given * maximum latency. Any messages that were either added to the batch, or @@ -106,19 +112,19 @@ public interface DatabaseComponent { throws DbException; /** - * Generates an offer for the given contact, or returns null if there are - * no messages to offer. + * Returns an offer for the given contact, or null if there are no messages + * to offer. */ Offer generateOffer(ContactId c, int maxMessages) throws DbException; /** - * Generates a retention ack for the given contact, or returns null if no + * Returns a retention ack for the given contact, or null if no retention * ack is due. */ RetentionAck generateRetentionAck(ContactId c) throws DbException; /** - * Generates a retention update for the given contact, for transmission + * Returns a retention update for the given contact, for transmission * over a transport with the given latency. Returns null if no update is * due. */ @@ -126,13 +132,13 @@ public interface DatabaseComponent { throws DbException; /** - * Generates a subscription ack for the given contact, or returns null if - * no ack is due. + * Returns a subscription ack for the given contact, or null if no + * subscription ack is due. */ SubscriptionAck generateSubscriptionAck(ContactId c) throws DbException; /** - * Generates a subscription update for the given contact, for transmission + * Returns a subscription update for the given contact, for transmission * over a transport with the given latency. Returns null if no update is * due. */ @@ -140,14 +146,14 @@ public interface DatabaseComponent { throws DbException; /** - * Generates a batch of transport acks for the given contact, or returns - * null if no acks are due. + * Returns a batch of transport acks for the given contact, or null if no + * transport acks are due. */ Collection<TransportAck> generateTransportAcks(ContactId c) throws DbException; /** - * Generates a batch of transport updates for the given contact, for + * Returns a batch of transport updates for the given contact, for * transmission over a transport with the given latency. Returns null if no * updates are due. */ @@ -169,8 +175,20 @@ public interface DatabaseComponent { /** Returns the group with the given ID, if the user subscribes to it. */ Group getGroup(GroupId g) throws DbException; - /** Returns the headers of all messages in the given group. */ - Collection<GroupMessageHeader> getGroupMessageHeaders(GroupId g) + /** Returns all groups to which the user subscribes. */ + Collection<Group> getGroups() throws DbException; + + /** + * Returns the ID of the inbox group for the given contact, or null if no + * inbox group has been set. + */ + GroupId getInboxGroup(ContactId c) throws DbException; + + /** + * Returns the headers of all messages in the inbox group for the given + * contact, or null if no inbox group has been set. + */ + Collection<MessageHeader> getInboxMessageHeaders(ContactId c) throws DbException; /** @@ -195,11 +213,8 @@ public interface DatabaseComponent { /** Returns the body of the message with the given ID. */ byte[] getMessageBody(MessageId m) throws DbException; - /** - * Returns the headers of all private messages to or from the given - * contact. - */ - Collection<PrivateMessageHeader> getPrivateMessageHeaders(ContactId c) + /** Returns the headers of all messages in the given group. */ + Collection<MessageHeader> getMessageHeaders(GroupId g) throws DbException; /** Returns true if the given message has been read. */ @@ -212,24 +227,15 @@ public interface DatabaseComponent { /** Returns all temporary secrets. */ Collection<TemporarySecret> getSecrets() throws DbException; - /** Returns the set of groups to which the user subscribes. */ - Collection<Group> getSubscriptions() throws DbException; - /** Returns the maximum latencies of all local transports. */ Map<TransportId, Long> getTransportLatencies() throws DbException; /** Returns the number of unread messages in each subscribed group. */ Map<GroupId, Integer> getUnreadMessageCounts() throws DbException; - /** Returns the contacts to which the given group is visible. */ + /** Returns the IDs of all contacts to which the given group is visible. */ Collection<ContactId> getVisibility(GroupId g) throws DbException; - /** Returns the subscriptions that are visible to the given contact. */ - Collection<GroupId> getVisibleSubscriptions(ContactId c) throws DbException; - - /** Returns true if any messages are sendable to the given contact. */ - boolean hasSendableMessages(ContactId c) throws DbException; - /** * Increments the outgoing connection counter for the given endpoint * in the given rotation period and returns the old value, or -1 if the @@ -295,6 +301,12 @@ public interface DatabaseComponent { /** Removes a contact (and all associated state) from the database. */ void removeContact(ContactId c) throws DbException; + /** + * Unsubscribes from a group. Any messages belonging to the group + * are deleted from the database. + */ + void removeGroup(Group g) throws DbException; + /** * Removes a local pseudonym (and all associated state) from the database. */ @@ -314,8 +326,14 @@ public interface DatabaseComponent { long centre, byte[] bitmap) throws DbException; /** - * Marks the given message read or unread and returns true if it was - * previously read. + * Makes a private group visible to the given contact, adds it to the + * contact's subscriptions, and sets it as the inbox group for the contact. + */ + public void setInboxGroup(ContactId c, Group g) throws DbException; + + /** + * Marks a message read or unread and returns true if it was previously + * read. */ boolean setReadFlag(MessageId m, boolean read) throws DbException; @@ -342,16 +360,4 @@ public interface DatabaseComponent { * current contacts. */ void setVisibleToAll(GroupId g, boolean all) throws DbException; - - /** - * Subscribes to the given group, or returns false if the user already has - * the maximum number of subscriptions. - */ - boolean subscribe(Group g) throws DbException; - - /** - * Unsubscribes from the given group. Any messages belonging to the group - * are deleted from the database. - */ - void unsubscribe(Group g) throws DbException; } diff --git a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java deleted file mode 100644 index e7f81f8b62..0000000000 --- a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.briar.api.db; - -import net.sf.briar.api.Author; -import net.sf.briar.api.messaging.GroupId; -import net.sf.briar.api.messaging.MessageId; - -public class GroupMessageHeader extends MessageHeader { - - private final GroupId groupId; - - public GroupMessageHeader(MessageId id, MessageId parent, Author author, - String contentType, long timestamp, boolean read, - GroupId groupId) { - super(id, parent, author, contentType, timestamp, read); - this.groupId = groupId; - } - - /** Returns the ID of the group to which the message belongs. */ - public GroupId getGroupId() { - return groupId; - } -} diff --git a/briar-api/src/net/sf/briar/api/db/MessageHeader.java b/briar-api/src/net/sf/briar/api/db/MessageHeader.java index 8c1dc20eb4..90822f583b 100644 --- a/briar-api/src/net/sf/briar/api/db/MessageHeader.java +++ b/briar-api/src/net/sf/briar/api/db/MessageHeader.java @@ -1,20 +1,23 @@ package net.sf.briar.api.db; import net.sf.briar.api.Author; +import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.MessageId; -public abstract class MessageHeader { +public class MessageHeader { private final MessageId id, parent; + private final GroupId groupId; private final Author author; private final String contentType; private final long timestamp; private final boolean read; - protected MessageHeader(MessageId id, MessageId parent, Author author, - String contentType, long timestamp, boolean read) { + public MessageHeader(MessageId id, MessageId parent, GroupId groupId, + Author author, String contentType, long timestamp, boolean read) { this.id = id; this.parent = parent; + this.groupId = groupId; this.author = author; this.contentType = contentType; this.timestamp = timestamp; @@ -34,6 +37,13 @@ public abstract class MessageHeader { return parent; } + /** + * Returns the unique identifier of the group to which the message belongs. + */ + public GroupId getGroupId() { + return groupId; + } + /** * Returns the message's author, or null if this is an anonymous message. */ diff --git a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java deleted file mode 100644 index 06abefef12..0000000000 --- a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.sf.briar.api.db; - -import net.sf.briar.api.Author; -import net.sf.briar.api.ContactId; -import net.sf.briar.api.messaging.MessageId; - -public class PrivateMessageHeader extends MessageHeader { - - private final ContactId contactId; - private final boolean incoming; - - public PrivateMessageHeader(MessageId id, MessageId parent, Author author, - String contentType, long timestamp, boolean read, - ContactId contactId, boolean incoming) { - super(id, parent, author, contentType, timestamp, read); - this.contactId = contactId; - this.incoming = incoming; - } - - /** - * Returns the ID of the contact who is the sender (if incoming) or - * recipient (if outgoing) of this message. - */ - public ContactId getContactId() { - return contactId; - } - - /** Returns true if this is an incoming message. */ - public boolean isIncoming() { - return incoming; - } -} diff --git a/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java deleted file mode 100644 index 40c5510301..0000000000 --- a/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.sf.briar.api.db.event; - -import net.sf.briar.api.messaging.Group; - -/** An event that is broadcast when a group message is added to the database. */ -public class GroupMessageAddedEvent extends DatabaseEvent { - - private final Group group; - private final boolean incoming; - - public GroupMessageAddedEvent(Group group, boolean incoming) { - this.group = group; - this.incoming = incoming; - } - - public Group getGroup() { - return group; - } - - public boolean isIncoming() { - return incoming; - } -} diff --git a/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java index 4b72402fa3..1d9c305437 100644 --- a/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java +++ b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorAddedEvent.java @@ -2,7 +2,7 @@ package net.sf.briar.api.db.event; import net.sf.briar.api.AuthorId; -/** An event that is broadcast when a pseudonym for the user is added. */ +/** An event that is broadcast when a local pseudonym is added. */ public class LocalAuthorAddedEvent extends DatabaseEvent { private final AuthorId authorId; diff --git a/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java index 0b5bd8849e..cf4e18e758 100644 --- a/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java +++ b/briar-api/src/net/sf/briar/api/db/event/LocalAuthorRemovedEvent.java @@ -2,7 +2,7 @@ package net.sf.briar.api.db.event; import net.sf.briar.api.AuthorId; -/** An event that is broadcast when a pseudonym for the user is removed. */ +/** An event that is broadcast when a local pseudonym is removed. */ public class LocalAuthorRemovedEvent extends DatabaseEvent { private final AuthorId authorId; diff --git a/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java new file mode 100644 index 0000000000..8f04e5d141 --- /dev/null +++ b/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java @@ -0,0 +1,29 @@ +package net.sf.briar.api.db.event; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.messaging.Group; + +/** An event that is broadcast when a message is added to the database. */ +public class MessageAddedEvent extends DatabaseEvent { + + private final Group group; + private final ContactId contactId; + + public MessageAddedEvent(Group group, ContactId contactId) { + this.group = group; + this.contactId = contactId; + } + + /** Returns the group to which the message belongs. */ + public Group getGroup() { + return group; + } + + /** + * Returns the ID of the contact from which the message was received, or + * null if the message was locally generated. + */ + public ContactId getContactId() { + return contactId; + } +} diff --git a/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java deleted file mode 100644 index 1ce1dafd37..0000000000 --- a/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.briar.api.db.event; - -import net.sf.briar.api.ContactId; - -/** - * An event that is broadcast when a private message is added to the database. - */ -public class PrivateMessageAddedEvent extends DatabaseEvent { - - private final ContactId contactId; - private final boolean incoming; - - public PrivateMessageAddedEvent(ContactId contactId, boolean incoming) { - this.contactId = contactId; - this.incoming = incoming; - } - - public ContactId getContactId() { - return contactId; - } - - public boolean isIncoming() { - return incoming; - } -} diff --git a/briar-api/src/net/sf/briar/api/messaging/Group.java b/briar-api/src/net/sf/briar/api/messaging/Group.java index a778b24dff..37b1d376c1 100644 --- a/briar-api/src/net/sf/briar/api/messaging/Group.java +++ b/briar-api/src/net/sf/briar/api/messaging/Group.java @@ -6,11 +6,13 @@ public class Group { private final GroupId id; private final String name; private final byte[] salt; + private final boolean isPrivate; - public Group(GroupId id, String name, byte[] salt) { + public Group(GroupId id, String name, byte[] salt, boolean isPrivate) { this.id = id; this.name = name; this.salt = salt; + this.isPrivate = isPrivate; } /** Returns the group's unique identifier. */ @@ -31,6 +33,11 @@ public class Group { return salt; } + /** Returns true if the group is private. */ + public boolean isPrivate() { + return isPrivate; + } + @Override public int hashCode() { return id.hashCode(); diff --git a/briar-api/src/net/sf/briar/api/messaging/GroupFactory.java b/briar-api/src/net/sf/briar/api/messaging/GroupFactory.java index 1535cab6cf..ea3d905342 100644 --- a/briar-api/src/net/sf/briar/api/messaging/GroupFactory.java +++ b/briar-api/src/net/sf/briar/api/messaging/GroupFactory.java @@ -1,12 +1,10 @@ package net.sf.briar.api.messaging; -import java.io.IOException; - public interface GroupFactory { /** Creates a group with the given name and a random salt. */ - Group createGroup(String name) throws IOException; + Group createGroup(String name, boolean isPrivate); /** Creates a group with the given name and salt. */ - Group createGroup(String name, byte[] salt) throws IOException; + Group createGroup(String name, byte[] salt, boolean isPrivate); } diff --git a/briar-api/src/net/sf/briar/api/messaging/MessageFactory.java b/briar-api/src/net/sf/briar/api/messaging/MessageFactory.java index 6e4dc2dd25..0c798ebb71 100644 --- a/briar-api/src/net/sf/briar/api/messaging/MessageFactory.java +++ b/briar-api/src/net/sf/briar/api/messaging/MessageFactory.java @@ -8,17 +8,10 @@ import net.sf.briar.api.crypto.PrivateKey; public interface MessageFactory { - /** Creates a private message. */ - Message createPrivateMessage(MessageId parent, String contentType, - long timestamp, byte[] body) throws IOException, - GeneralSecurityException; - - /** Creates an anonymous group message. */ Message createAnonymousMessage(MessageId parent, Group group, String contentType, long timestamp, byte[] body) throws IOException, GeneralSecurityException; - /** Creates a pseudonymous group message. */ Message createPseudonymousMessage(MessageId parent, Group group, Author author, PrivateKey privateKey, String contentType, long timestamp, byte[] body) throws IOException, diff --git a/briar-api/src/net/sf/briar/api/messaging/MessagingConstants.java b/briar-api/src/net/sf/briar/api/messaging/MessagingConstants.java index 80a1d7ddfe..3b1347f91c 100644 --- a/briar-api/src/net/sf/briar/api/messaging/MessagingConstants.java +++ b/briar-api/src/net/sf/briar/api/messaging/MessagingConstants.java @@ -11,7 +11,7 @@ public interface MessagingConstants { */ int MAX_PACKET_LENGTH = MIN_CONNECTION_LENGTH / 2; - /** The maximum number of groups a user may subscribe to. */ + /** The maximum number of public groups a user may subscribe to. */ int MAX_SUBSCRIPTIONS = 3000; /** The maximum length of a group's name in UTF-8 bytes. */ diff --git a/briar-api/src/net/sf/briar/api/messaging/SubscriptionUpdate.java b/briar-api/src/net/sf/briar/api/messaging/SubscriptionUpdate.java index 337d23b991..48c3cda3c9 100644 --- a/briar-api/src/net/sf/briar/api/messaging/SubscriptionUpdate.java +++ b/briar-api/src/net/sf/briar/api/messaging/SubscriptionUpdate.java @@ -5,11 +5,11 @@ import java.util.Collection; /** A packet updating the recipient's view of the sender's subscriptions. */ public class SubscriptionUpdate { - private final Collection<Group> subs; + private final Collection<Group> groups; private final long version; - public SubscriptionUpdate(Collection<Group> subs, long version) { - this.subs = subs; + public SubscriptionUpdate(Collection<Group> groups, long version) { + this.groups = groups; this.version = version; } @@ -18,7 +18,7 @@ public class SubscriptionUpdate { * has made visible to the recipient. */ public Collection<Group> getGroups() { - return subs; + return groups; } /** Returns the update's version number. */ diff --git a/briar-api/src/net/sf/briar/api/messaging/UnverifiedMessage.java b/briar-api/src/net/sf/briar/api/messaging/UnverifiedMessage.java index bbea6bdd4d..880d3b3d1a 100644 --- a/briar-api/src/net/sf/briar/api/messaging/UnverifiedMessage.java +++ b/briar-api/src/net/sf/briar/api/messaging/UnverifiedMessage.java @@ -8,19 +8,18 @@ public class UnverifiedMessage { private final MessageId parent; private final Group group; private final Author author; - private final String contentType, subject; + private final String contentType; private final long timestamp; private final byte[] raw, signature; private final int bodyStart, bodyLength, signedLength; public UnverifiedMessage(MessageId parent, Group group, Author author, - String contentType, String subject, long timestamp, byte[] raw, - byte[] signature, int bodyStart, int bodyLength, int signedLength) { + String contentType, long timestamp, byte[] raw, byte[] signature, + int bodyStart, int bodyLength, int signedLength) { this.parent = parent; this.group = group; this.author = author; this.contentType = contentType; - this.subject = subject; this.timestamp = timestamp; this.raw = raw; this.signature = signature; @@ -58,15 +57,6 @@ public class UnverifiedMessage { return contentType; } - /** - * Returns the message's subject line, which is created from the first 50 - * bytes of the message body if the content type is text/plain, or is the - * empty string otherwise. - */ - public String getSubject() { - return subject; - } - /** Returns the message's timestamp. */ public long getTimestamp() { return timestamp; diff --git a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java index 682fc7071d..566aec70b2 100644 --- a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java +++ b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java @@ -64,6 +64,7 @@ class CryptoComponentImpl implements CryptoComponent { // Labels for secret derivation private static final byte[] MASTER = { 'M', 'A', 'S', 'T', 'E', 'R', '\0' }; + private static final byte[] SALT = { 'S', 'A', 'L', 'T', '\0' }; private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T', '\0' }; private static final byte[] ROTATE = { 'R', 'O', 'T', 'A', 'T', 'E', '\0' }; // Label for confirmation code derivation @@ -235,6 +236,14 @@ class CryptoComponentImpl implements CryptoComponent { return agreement.calculateAgreement(ecPub).toByteArray(); } + public byte[] deriveGroupSalt(byte[] secret) { + if(secret.length != CIPHER_KEY_BYTES) + throw new IllegalArgumentException(); + if(Arrays.equals(secret, BLANK_SECRET)) + throw new IllegalArgumentException(); + return counterModeKdf(secret, SALT, 0); + } + public byte[] deriveInitialSecret(byte[] secret, int transportIndex) { if(secret.length != CIPHER_KEY_BYTES) throw new IllegalArgumentException(); diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java index 1a10cc82db..82b20351b8 100644 --- a/briar-core/src/net/sf/briar/db/Database.java +++ b/briar-core/src/net/sf/briar/db/Database.java @@ -13,8 +13,7 @@ import net.sf.briar.api.TransportConfig; import net.sf.briar.api.TransportId; import net.sf.briar.api.TransportProperties; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.GroupMessageHeader; -import net.sf.briar.api.db.PrivateMessageHeader; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.GroupStatus; @@ -79,8 +78,8 @@ interface Database<T> { void commitTransaction(T txn) throws DbException; /** - * Stores a contact with the given pseudonym, associated with the given - * local pseudonym, and returns an ID for the contact. + * Stores a contact associated with the given local and remote pseudonyms, + * and returns an ID for the contact. * <p> * Locking: contact write, message write, retention write, * subscription write, transport write, window write. @@ -96,13 +95,12 @@ interface Database<T> { void addEndpoint(T txn, Endpoint ep) throws DbException; /** - * Stores the given message, or returns false if the message is already in - * the database. + * Subscribes to a group, or returns false if the user already has the + * maximum number of subscriptions. * <p> - * Locking: message write. + * Locking: message write, subscription write. */ - boolean addGroupMessage(T txn, Message m, boolean incoming) - throws DbException; + boolean addGroup(T txn, Group g) throws DbException; /** * Stores a local pseudonym. @@ -113,20 +111,18 @@ interface Database<T> { void addLocalAuthor(T txn, LocalAuthor a) throws DbException; /** - * Records a received message as needing to be acknowledged. + * Stores a message. * <p> * Locking: message write. */ - void addMessageToAck(T txn, ContactId c, MessageId m) throws DbException; + void addMessage(T txn, Message m, boolean incoming) throws DbException; /** - * Stores the given message, or returns false if the message is already in - * the database. + * Records a received message as needing to be acknowledged. * <p> * Locking: message write. */ - boolean addPrivateMessage(T txn, Message m, ContactId c, boolean incoming) - throws DbException; + void addMessageToAck(T txn, ContactId c, MessageId m) throws DbException; /** * Stores the given temporary secrets and deletes any secrets that have @@ -146,14 +142,6 @@ interface Database<T> { void addStatus(T txn, ContactId c, MessageId m, boolean seen) throws DbException; - /** - * Subscribes to the given group, or returns false if the user already has - * the maximum number of subscriptions. - * <p> - * Locking: subscription write. - */ - boolean addSubscription(T txn, Group g) throws DbException; - /** * Stores a transport and returns true if the transport was not previously * in the database. @@ -184,6 +172,13 @@ interface Database<T> { */ boolean containsContact(T txn, ContactId c) throws DbException; + /** + * Returns true if the user subscribes to the given group. + * <p> + * Locking: subscription read. + */ + boolean containsGroup(T txn, GroupId g) throws DbException; + /** * Returns true if the database contains the given local pseudonym. * <p> @@ -199,11 +194,11 @@ interface Database<T> { boolean containsMessage(T txn, MessageId m) throws DbException; /** - * Returns true if the user subscribes to the given group. + * Returns true if any messages are sendable to the given contact. * <p> - * Locking: subscription read. + * Locking: message read, subscription read. */ - boolean containsSubscription(T txn, GroupId g) throws DbException; + boolean containsSendableMessages(T txn, ContactId c) throws DbException; /** * Returns true if the database contains the given transport. @@ -213,12 +208,12 @@ interface Database<T> { boolean containsTransport(T txn, TransportId t) throws DbException; /** - * Returns true if the user subscribes to the given group and the - * subscription is visible to the given contact. + * Returns true if the user subscribes to the given group and the group is + * visible to the given contact. * <p> * Locking: subscription read. */ - boolean containsVisibleSubscription(T txn, ContactId c, GroupId g) + boolean containsVisibleGroup(T txn, ContactId c, GroupId g) throws DbException; /** @@ -279,25 +274,34 @@ interface Database<T> { /** * Returns the group with the given ID, if the user subscribes to it. + * <p> + * Locking: subscription read. */ Group getGroup(T txn, GroupId g) throws DbException; /** - * Returns the headers of all messages in the given group. + * Returns all groups to which the user subscribes. * <p> - * Locking: message read. + * Locking: subscription read. */ - Collection<GroupMessageHeader> getGroupMessageHeaders(T txn, GroupId g) - throws DbException; + Collection<Group> getGroups(T txn) throws DbException; /** - * Returns the parent of the given group message, or null if either the - * message has no parent, or the parent is absent from the database, or the - * parent belongs to a different group. + * Returns the ID of the inbox group for the given contact, or null if no + * inbox group has been set. * <p> - * Locking: message read. + * Locking: contact read, subscription read. */ - MessageId getGroupMessageParent(T txn, MessageId m) throws DbException; + GroupId getInboxGroup(T txn, ContactId c) throws DbException; + + /** + * Returns the headers of all messages in the inbox group for the given + * contact, or null if no inbox group has been set. + * <p> + * Locking: contact read, identity read, message read, subscription read. + */ + Collection<MessageHeader> getInboxMessageHeaders(T txn, ContactId c) + throws DbException; /** * Returns the time at which a connection to each contact was last opened @@ -345,17 +349,16 @@ interface Database<T> { byte[] getMessageBody(T txn, MessageId m) throws DbException; /** - * Returns the headers of all private messages to or from the given - * contact. + * Returns the headers of all messages in the given group. * <p> - * Locking: contact read, identity read, message read. + * Locking: message read. */ - Collection<PrivateMessageHeader> getPrivateMessageHeaders(T txn, - ContactId c) throws DbException; + Collection<MessageHeader> getMessageHeaders(T txn, GroupId g) + throws DbException; /** - * Returns the IDs of some messages received from the given contact that - * need to be acknowledged, up to the given number of messages. + * Returns the IDs of messages received from the given contact that need + * to be acknowledged, up to the given number of messages. * <p> * Locking: message read. */ @@ -371,6 +374,23 @@ interface Database<T> { Collection<MessageId> getMessagesToOffer(T txn, ContactId c, int maxMessages) throws DbException; + /** + * Returns the IDs of the oldest messages in the database, with a total + * size less than or equal to the given size. + * <p> + * Locking: message read. + */ + Collection<MessageId> getOldMessages(T txn, int size) throws DbException; + + /** + * Returns the parent of the given message, or null if either the message + * has no parent, or the parent is absent from the database, or the parent + * belongs to a different group. + * <p> + * Locking: message read. + */ + MessageId getParent(T txn, MessageId m) throws DbException; + /** * Returns the message identified by the given ID, in serialised form. * <p> @@ -388,14 +408,6 @@ interface Database<T> { byte[] getRawMessageIfSendable(T txn, ContactId c, MessageId m) throws DbException; - /** - * Returns the IDs of the oldest messages in the database, with a total - * size less than or equal to the given size. - * <p> - * Locking: message read. - */ - Collection<MessageId> getOldMessages(T txn, int size) throws DbException; - /** * Returns true if the given message has been read. * <p> @@ -444,20 +456,6 @@ interface Database<T> { Collection<MessageId> getSendableMessages(T txn, ContactId c, int maxLength) throws DbException; - /** - * Returns the groups to which the user subscribes. - * <p> - * Locking: subscription read. - */ - Collection<Group> getSubscriptions(T txn) throws DbException; - - /** - * Returns the groups to which the given contact subscribes. - * <p> - * Locking: subscription read. - */ - Collection<Group> getSubscriptions(T txn, ContactId c) throws DbException; - /** * Returns a subscription ack for the given contact, or null if no ack is * due. @@ -518,27 +516,21 @@ interface Database<T> { Map<GroupId, Integer> getUnreadMessageCounts(T txn) throws DbException; /** - * Returns the contacts to which the given group is visible. + * Returns the IDs of all contacts to which the given group is visible. * <p> * Locking: subscription read. */ Collection<ContactId> getVisibility(T txn, GroupId g) throws DbException; /** - * Returns the subscriptions that are visible to the given contact. + * Returns the IDs of all private groups that are visible to the given + * contact. * <p> * Locking: subscription read. */ - Collection<GroupId> getVisibleSubscriptions(T txn, ContactId c) + Collection<GroupId> getVisiblePrivateGroups(T txn, ContactId c) throws DbException; - /** - * Returns true if any messages are sendable to the given contact. - * <p> - * Locking: message read, subscription read. - */ - boolean hasSendableMessages(T txn, ContactId c) throws DbException; - /** * Increments the outgoing connection counter for the given endpoint * in the given rotation period and returns the old value, or -1 if the @@ -576,7 +568,7 @@ interface Database<T> { throws DbException; /** - * Removes a contact (and all associated state) from the database. + * Removes a contact from the database. * <p> * Locking: contact write, message write, retention write, * subscription write, transport write, window write. @@ -584,8 +576,16 @@ interface Database<T> { void removeContact(T txn, ContactId c) throws DbException; /** - * Removes the local pseudonym with the given ID (and all associated - * state) from the database. + * Unsubscribes from a group. Any messages belonging to the group are + * deleted from the database. + * <p> + * Locking: message write, subscription write. + */ + void removeGroup(T txn, GroupId g) throws DbException; + + /** + * Removes a local pseudonym (and all associated contacts) from the + * database. * <p> * Locking: contact write, identity write, message write, retention write, * subscription write, transport write, window write. @@ -608,23 +608,6 @@ interface Database<T> { void removeMessagesToAck(T txn, ContactId c, Collection<MessageId> acked) throws DbException; - /** - * Marks any of the given messages that are considered outstanding with - * respect to the given contact as seen by the contact. - * <p> - * Locking: message write. - */ - void removeOutstandingMessages(T txn, ContactId c, - Collection<MessageId> acked) throws DbException; - - /** - * Unsubscribes from the given group. Any messages belonging to the group - * are deleted from the database. - * <p> - * Locking: message write, subscription write. - */ - void removeSubscription(T txn, GroupId g) throws DbException; - /** * Removes a transport (and all associated state) from the database. * <p> @@ -648,6 +631,24 @@ interface Database<T> { void setConnectionWindow(T txn, ContactId c, TransportId t, long period, long centre, byte[] bitmap) throws DbException; + /** + * Updates the groups to which the given contact subscribes and returns + * true, unless an update with an equal or higher version number has + * already been received from the contact. + * <p> + * Locking: subscription write. + */ + boolean setGroups(T txn, ContactId c, Collection<Group> groups, + long version) throws DbException; + + /** + * Makes a private group visible to the given contact, adds it to the + * contact's subscriptions, and sets it as the inbox group for the contact. + * <p> + * Locking: contact read, message write, subscription write. + */ + public void setInboxGroup(T txn, ContactId c, Group g) throws DbException; + /** * Sets the time at which a connection to the given contact was last made. * <p> @@ -656,8 +657,8 @@ interface Database<T> { void setLastConnected(T txn, ContactId c, long now) throws DbException; /** - * Marks the given message read or unread and returns true if it was - * previously read. + * Marks a message read or unread and returns true if it was previously + * read. * <p> * Locking: message write. */ @@ -703,16 +704,6 @@ interface Database<T> { boolean setStatusSeenIfVisible(T txn, ContactId c, MessageId m) throws DbException; - /** - * Updates the groups to which the given contact subscribes and returns - * true, unless an update with an equal or higher version number has - * already been received from the contact. - * <p> - * Locking: subscription write. - */ - boolean setSubscriptions(T txn, ContactId c, Collection<Group> subs, - long version) throws DbException; - /** * Records a retention ack from the given contact for the given version, * unless the contact has already acked an equal or higher version. diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index d7c45f52a4..9999486989 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -2,7 +2,6 @@ package net.sf.briar.db; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import static net.sf.briar.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; import static net.sf.briar.db.DatabaseConstants.BYTES_PER_SWEEP; import static net.sf.briar.db.DatabaseConstants.CRITICAL_FREE_SPACE; import static net.sf.briar.db.DatabaseConstants.MAX_BYTES_BETWEEN_SPACE_CHECKS; @@ -37,26 +36,24 @@ import net.sf.briar.api.db.AckAndRequest; import net.sf.briar.api.db.ContactExistsException; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.db.LocalAuthorExistsException; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.db.NoSuchContactException; import net.sf.briar.api.db.NoSuchLocalAuthorException; import net.sf.briar.api.db.NoSuchMessageException; import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.NoSuchTransportException; -import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.db.event.ContactAddedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.GroupMessageAddedEvent; import net.sf.briar.api.db.event.LocalAuthorAddedEvent; import net.sf.briar.api.db.event.LocalAuthorRemovedEvent; import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent; +import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.MessageReceivedEvent; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent; @@ -274,6 +271,31 @@ DatabaseCleaner.Callback { } } + public boolean addGroup(Group g) throws DbException { + boolean added = false; + messageLock.writeLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsGroup(txn, g.getId())) + added = db.addGroup(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + if(added) callListeners(new SubscriptionAddedEvent(g)); + return added; + } + public void addLocalAuthor(LocalAuthor a) throws DbException { contactLock.writeLock().lock(); try { @@ -323,8 +345,8 @@ DatabaseCleaner.Callback { callListeners(new LocalAuthorAddedEvent(a.getId())); } - public void addLocalGroupMessage(Message m) throws DbException { - boolean added = false; + public void addLocalMessage(Message m) throws DbException { + boolean duplicate; contactLock.readLock().lock(); try { messageLock.writeLock().lock(); @@ -333,11 +355,12 @@ DatabaseCleaner.Callback { try { T txn = db.startTransaction(); try { - // Don't store the message if the user has - // unsubscribed from the group - GroupId g = m.getGroup().getId(); - if(db.containsSubscription(txn, g)) - added = storeGroupMessage(txn, m, null); + duplicate = db.containsMessage(txn, m.getId()); + if(!duplicate) { + GroupId g = m.getGroup().getId(); + if(db.containsGroup(txn, g)) + addMessage(txn, m, null); + } db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -352,91 +375,30 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } - if(added) - callListeners(new GroupMessageAddedEvent(m.getGroup(), false)); + if(!duplicate) + callListeners(new MessageAddedEvent(m.getGroup(), null)); } /** - * If the given message is already in the database, marks it as seen by the - * sender and returns false. Otherwise stores the message, marks it as seen - * by the sender and unseen by all other contacts, and returns true. + * Stores the given message, marks it as read if it was locally generated, + * otherwise marks it as seen by the sender, and marks it as unseen by all + * other contacts. * <p> - * Locking: contact read, message write. - * @param sender is null for a locally generated message. + * Locking: contact read, message write, subscription read. + * @param sender null for a locally generated message. */ - private boolean storeGroupMessage(T txn, Message m, ContactId sender) + private void addMessage(T txn, Message m, ContactId sender) throws DbException { - if(m.getGroup() == null) throw new IllegalArgumentException(); - boolean stored = db.addGroupMessage(txn, m, sender != null); - if(stored && sender == null) db.setReadFlag(txn, m.getId(), true); - // Mark the message as seen by the sender + db.addMessage(txn, m, sender != null); MessageId id = m.getId(); - if(sender != null) db.addStatus(txn, sender, id, true); - if(stored) { - // Mark the message as unseen by other contacts - for(ContactId c : db.getContactIds(txn)) - if(!c.equals(sender)) db.addStatus(txn, c, id, false); - // Count the bytes stored - synchronized(spaceLock) { - bytesStoredSinceLastCheck += m.getSerialised().length; - } - } else { - if(LOG.isLoggable(INFO)) - LOG.info("Duplicate group message not stored"); - } - return stored; - } - - public void addLocalPrivateMessage(Message m, ContactId c) - throws DbException { - boolean added; - contactLock.readLock().lock(); - try { - messageLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - added = storePrivateMessage(txn, m, c, false); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - messageLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - if(added) callListeners(new PrivateMessageAddedEvent(c, false)); - } - - /** - * If the given message is already in the database, returns false. - * Otherwise stores the message and marks it as seen or unseen with respect - * to the given contact, depending on whether the message is incoming or - * outgoing, respectively. - * <p> - * Locking: message write. - */ - private boolean storePrivateMessage(T txn, Message m, ContactId c, - boolean incoming) throws DbException { - if(m.getGroup() != null) throw new IllegalArgumentException(); - if(m.getAuthor() != null) throw new IllegalArgumentException(); - if(!db.addPrivateMessage(txn, m, c, incoming)) { - if(LOG.isLoggable(INFO)) - LOG.info("Duplicate private message not stored"); - return false; - } - if(!incoming) db.setReadFlag(txn, m.getId(), true); - db.addStatus(txn, c, m.getId(), incoming); + if(sender == null) db.setReadFlag(txn, id, true); + else db.addStatus(txn, sender, id, true); + for(ContactId c : db.getContactIds(txn)) + if(!c.equals(sender)) db.addStatus(txn, c, id, false); // Count the bytes stored synchronized(spaceLock) { bytesStoredSinceLastCheck += m.getSerialised().length; } - return true; } public void addSecrets(Collection<TemporarySecret> secrets) @@ -500,6 +462,35 @@ DatabaseCleaner.Callback { return added; } + public boolean containsSendableMessages(ContactId c) throws DbException { + contactLock.readLock().lock(); + try { + messageLock.readLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + boolean has = db.containsSendableMessages(txn, c); + db.commitTransaction(txn); + return has; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + public Ack generateAck(ContactId c, int maxMessages) throws DbException { Collection<MessageId> acked; contactLock.readLock().lock(); @@ -924,7 +915,7 @@ DatabaseCleaner.Callback { try { T txn = db.startTransaction(); try { - if(!db.containsSubscription(txn, g)) + if(!db.containsGroup(txn, g)) throw new NoSuchSubscriptionException(); Group group = db.getGroup(txn, g); db.commitTransaction(txn); @@ -938,20 +929,35 @@ DatabaseCleaner.Callback { } } - public Collection<GroupMessageHeader> getGroupMessageHeaders(GroupId g) - throws DbException { - messageLock.readLock().lock(); + public Collection<Group> getGroups() throws DbException { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + Collection<Group> groups = db.getGroups(txn); + db.commitTransaction(txn); + return groups; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } + + public GroupId getInboxGroup(ContactId c) throws DbException { + contactLock.readLock().lock(); try { subscriptionLock.readLock().lock(); try { T txn = db.startTransaction(); try { - if(!db.containsSubscription(txn, g)) - throw new NoSuchSubscriptionException(); - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(txn, g); + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + GroupId inbox = db.getInboxGroup(txn, c); db.commitTransaction(txn); - return headers; + return inbox; } catch(DbException e) { db.abortTransaction(txn); throw e; @@ -960,7 +966,43 @@ DatabaseCleaner.Callback { subscriptionLock.readLock().unlock(); } } finally { - messageLock.readLock().unlock(); + contactLock.readLock().unlock(); + } + } + + public Collection<MessageHeader> getInboxMessageHeaders(ContactId c) + throws DbException { + contactLock.readLock().lock(); + try { + identityLock.readLock().lock(); + try { + messageLock.readLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + Collection<MessageHeader> headers = + db.getInboxMessageHeaders(txn, c); + db.commitTransaction(txn); + return headers; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + } finally { + identityLock.readLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); } } @@ -1075,32 +1117,29 @@ DatabaseCleaner.Callback { } } - public Collection<PrivateMessageHeader> getPrivateMessageHeaders( - ContactId c) throws DbException { - contactLock.readLock().lock(); + public Collection<MessageHeader> getMessageHeaders(GroupId g) + throws DbException { + messageLock.readLock().lock(); try { - identityLock.readLock().lock(); + subscriptionLock.readLock().lock(); try { - messageLock.readLock().lock(); + T txn = db.startTransaction(); try { - T txn = db.startTransaction(); - try { - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(txn, c); - db.commitTransaction(txn); - return headers; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - messageLock.readLock().unlock(); + if(!db.containsGroup(txn, g)) + throw new NoSuchSubscriptionException(); + Collection<MessageHeader> headers = + db.getMessageHeaders(txn, g); + db.commitTransaction(txn); + return headers; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; } } finally { - identityLock.readLock().unlock(); + subscriptionLock.readLock().unlock(); } } finally { - contactLock.readLock().unlock(); + messageLock.readLock().unlock(); } } @@ -1159,23 +1198,6 @@ DatabaseCleaner.Callback { } } - public Collection<Group> getSubscriptions() throws DbException { - subscriptionLock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - Collection<Group> subs = db.getSubscriptions(txn); - db.commitTransaction(txn); - return subs; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.readLock().unlock(); - } - } - public Map<TransportId, Long> getTransportLatencies() throws DbException { transportLock.readLock().lock(); try { @@ -1216,7 +1238,7 @@ DatabaseCleaner.Callback { try { T txn = db.startTransaction(); try { - if(!db.containsSubscription(txn, g)) + if(!db.containsGroup(txn, g)) throw new NoSuchSubscriptionException(); Collection<ContactId> visible = db.getVisibility(txn, g); db.commitTransaction(txn); @@ -1230,61 +1252,6 @@ DatabaseCleaner.Callback { } } - public Collection<GroupId> getVisibleSubscriptions(ContactId c) - throws DbException { - contactLock.readLock().lock(); - try { - subscriptionLock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - Collection<GroupId> visible = - db.getVisibleSubscriptions(txn, c); - db.commitTransaction(txn); - return visible; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.readLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - public boolean hasSendableMessages(ContactId c) throws DbException { - contactLock.readLock().lock(); - try { - messageLock.readLock().lock(); - try { - subscriptionLock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - boolean has = db.hasSendableMessages(txn, c); - db.commitTransaction(txn); - return has; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.readLock().unlock(); - } - } finally { - messageLock.readLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - public long incrementConnectionCounter(ContactId c, TransportId t, long period) throws DbException { contactLock.readLock().lock(); @@ -1374,7 +1341,8 @@ DatabaseCleaner.Callback { try { if(!db.containsContact(txn, c)) throw new NoSuchContactException(); - db.removeOutstandingMessages(txn, c, a.getMessageIds()); + for(MessageId m : a.getMessageIds()) + db.setStatusSeenIfVisible(txn, c, m); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -1389,7 +1357,7 @@ DatabaseCleaner.Callback { } public void receiveMessage(ContactId c, Message m) throws DbException { - boolean added = false; + boolean duplicate, visible; contactLock.readLock().lock(); try { messageLock.writeLock().lock(); @@ -1400,8 +1368,11 @@ DatabaseCleaner.Callback { try { if(!db.containsContact(txn, c)) throw new NoSuchContactException(); - added = storeMessage(txn, c, m); - db.addMessageToAck(txn, c, m.getId()); + duplicate = db.containsMessage(txn, m.getId()); + GroupId g = m.getGroup().getId(); + visible = db.containsVisibleGroup(txn, c, g); + if(!duplicate && visible) addMessage(txn, m, c); + if(visible) db.addMessageToAck(txn, c, m.getId()); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -1416,36 +1387,8 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } - callListeners(new MessageReceivedEvent(c)); - if(added) { - Group g = m.getGroup(); - if(g == null) callListeners(new PrivateMessageAddedEvent(c, true)); - else callListeners(new GroupMessageAddedEvent(g, true)); - } - } - - /** - * Attempts to store a message received from the given contact, and returns - * true if it was stored. - * <p> - * Locking: contact read, message write, subscription read. - */ - private boolean storeMessage(T txn, ContactId c, Message m) - throws DbException { - long now = clock.currentTimeMillis(); - if(m.getTimestamp() > now + MAX_CLOCK_DIFFERENCE) { - if(LOG.isLoggable(INFO)) - LOG.info("Discarding message with future timestamp"); - return false; - } - Group g = m.getGroup(); - if(g == null) return storePrivateMessage(txn, m, c, true); - if(!db.containsVisibleSubscription(txn, c, g.getId())) { - if(LOG.isLoggable(INFO)) - LOG.info("Discarding message without visible subscription"); - return false; - } - return storeGroupMessage(txn, m, c); + if(visible) callListeners(new MessageReceivedEvent(c)); + if(!duplicate) callListeners(new MessageAddedEvent(m.getGroup(), c)); } public AckAndRequest receiveOffer(ContactId c, Offer o) throws DbException { @@ -1573,7 +1516,7 @@ DatabaseCleaner.Callback { throw new NoSuchContactException(); Collection<Group> groups = u.getGroups(); long version = u.getVersion(); - updated = db.setSubscriptions(txn, c, groups, version); + updated = db.setGroups(txn, c, groups, version); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); @@ -1689,6 +1632,34 @@ DatabaseCleaner.Callback { callListeners(new ContactRemovedEvent(c)); } + public void removeGroup(Group g) throws DbException { + Collection<ContactId> affected; + messageLock.writeLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + GroupId id = g.getId(); + if(!db.containsGroup(txn, id)) + throw new NoSuchSubscriptionException(); + affected = db.getVisibility(txn, id); + db.removeGroup(txn, id); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + callListeners(new SubscriptionRemovedEvent(g)); + callListeners(new LocalSubscriptionsUpdatedEvent(affected)); + } + public void removeLocalAuthor(AuthorId a) throws DbException { Collection<ContactId> affected; contactLock.writeLock().lock(); @@ -1798,6 +1769,35 @@ DatabaseCleaner.Callback { } } + public void setInboxGroup(ContactId c, Group g) throws DbException { + if(!g.isPrivate()) throw new IllegalArgumentException(); + contactLock.readLock().lock(); + try { + messageLock.writeLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + db.setInboxGroup(txn, c, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + public boolean setReadFlag(MessageId m, boolean read) throws DbException { messageLock.writeLock().lock(); try { @@ -1880,7 +1880,7 @@ DatabaseCleaner.Callback { try { T txn = db.startTransaction(); try { - if(!db.containsSubscription(txn, g)) + if(!db.containsGroup(txn, g)) throw new NoSuchSubscriptionException(); // Use HashSets for O(1) lookups, O(n) overall running time HashSet<ContactId> now = new HashSet<ContactId>(visible); @@ -1923,7 +1923,7 @@ DatabaseCleaner.Callback { try { T txn = db.startTransaction(); try { - if(!db.containsSubscription(txn, g)) + if(!db.containsGroup(txn, g)) throw new NoSuchSubscriptionException(); // Make the group visible or invisible to future contacts db.setVisibleToAll(txn, g, all); @@ -1953,59 +1953,6 @@ DatabaseCleaner.Callback { callListeners(new LocalSubscriptionsUpdatedEvent(affected)); } - public boolean subscribe(Group g) throws DbException { - boolean added = false; - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsSubscription(txn, g.getId())) - added = db.addSubscription(txn, g); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - if(added) callListeners(new SubscriptionAddedEvent(g)); - return added; - } - - public void unsubscribe(Group g) throws DbException { - Collection<ContactId> affected; - identityLock.writeLock().lock(); - try { - messageLock.writeLock().lock(); - try { - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - GroupId id = g.getId(); - if(!db.containsSubscription(txn, id)) - throw new NoSuchSubscriptionException(); - affected = db.getVisibility(txn, id); - db.removeSubscription(txn, id); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - } finally { - messageLock.writeLock().unlock(); - } - } finally { - identityLock.writeLock().unlock(); - } - callListeners(new SubscriptionRemovedEvent(g)); - callListeners(new LocalSubscriptionsUpdatedEvent(affected)); - } - public void checkFreeSpaceAndClean() throws DbException { long freeSpace = db.getFreeSpace(); if(LOG.isLoggable(INFO)) LOG.info(freeSpace + " bytes free space"); diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index 2c432b5c66..6fe38d2d25 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -18,12 +18,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.logging.Logger; import net.sf.briar.api.Author; @@ -37,8 +35,7 @@ import net.sf.briar.api.TransportProperties; import net.sf.briar.api.clock.Clock; import net.sf.briar.api.db.DbClosedException; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.GroupMessageHeader; -import net.sf.briar.api.db.PrivateMessageHeader; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.GroupStatus; @@ -91,6 +88,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " (groupId HASH NOT NULL," + " name VARCHAR NOT NULL," + " salt BINARY NOT NULL," + + " private BOOLEAN NOT NULL," + " visibleToAll BOOLEAN NOT NULL," + " PRIMARY KEY (groupId))"; @@ -99,6 +97,7 @@ abstract class JdbcDatabase implements Database<Connection> { "CREATE TABLE groupVisibilities" + " (contactId INT NOT NULL," + " groupId HASH NOT NULL," + + " inbox BOOLEAN NOT NULL," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" + " ON DELETE CASCADE," @@ -113,6 +112,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " groupId HASH NOT NULL," // Not a foreign key + " name VARCHAR NOT NULL," + " salt BINARY NOT NULL," + + " private BOOLEAN NOT NULL," + " PRIMARY KEY (contactId, groupId)," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" @@ -138,7 +138,7 @@ abstract class JdbcDatabase implements Database<Connection> { "CREATE TABLE messages" + " (messageId HASH NOT NULL," + " parentId HASH," // Null for the first msg in a thread - + " groupId HASH," // Null for private messages + + " groupId HASH NOT NULL," + " authorId HASH," // Null for private/anon messages + " authorName VARCHAR," // Null for private/anon messages + " authorKey VARCHAR," // Null for private/anon messages @@ -149,14 +149,10 @@ abstract class JdbcDatabase implements Database<Connection> { + " bodyLength INT NOT NULL," + " raw BLOB NOT NULL," + " incoming BOOLEAN NOT NULL," - + " contactId INT UNSIGNED," // Null for group messages + " read BOOLEAN NOT NULL," + " PRIMARY KEY (messageId)," + " FOREIGN KEY (groupId)" + " REFERENCES groups (groupId)" - + " ON DELETE CASCADE," - + " FOREIGN KEY (contactId)" - + " REFERENCES contacts (contactId)" + " ON DELETE CASCADE)"; private static final String INDEX_MESSAGES_BY_TIMESTAMP = @@ -546,8 +542,9 @@ abstract class JdbcDatabase implements Database<Connection> { rs.close(); ps.close(); if(!ids.isEmpty()) { - sql = "INSERT INTO groupVisibilities (contactId, groupId)" - + " VALUES (?, ?)"; + sql = "INSERT INTO groupVisibilities" + + " (contactId, groupId, inbox)" + + " VALUES (?, ?, FALSE)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); for(byte[] id : ids) { @@ -644,18 +641,80 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean addGroupMessage(Connection txn, Message m, boolean incoming) + public boolean addGroup(Connection txn, Group g) throws DbException { + if(maximumSubscriptionsReached(txn)) return false; + PreparedStatement ps = null; + try { + String sql = "INSERT INTO groups" + + " (groupId, name, salt, private, visibleToAll)" + + " VALUES (?, ?, ?, ?, FALSE)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getId().getBytes()); + ps.setString(2, g.getName()); + ps.setBytes(3, g.getSalt()); + ps.setBoolean(4, g.isPrivate()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + return true; + } catch(SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + + private boolean maximumSubscriptionsReached(Connection txn) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT COUNT (groupId) FROM groups"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + if(!rs.next()) throw new DbStateException(); + int count = rs.getInt(1); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + if(count > MAX_SUBSCRIPTIONS) throw new DbStateException(); + return count == MAX_SUBSCRIPTIONS; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public void addLocalAuthor(Connection txn, LocalAuthor a) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO localAuthors" + + " (authorId, name, publicKey, privateKey)" + + " VALUES (?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getId().getBytes()); + ps.setString(2, a.getName()); + ps.setBytes(3, a.getPublicKey()); + ps.setBytes(4, a.getPrivateKey()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + + public void addMessage(Connection txn, Message m, boolean incoming) throws DbException { - if(m.getGroup() == null) throw new IllegalArgumentException(); - if(containsMessage(txn, m.getId())) return false; PreparedStatement ps = null; try { String sql = "INSERT INTO messages (messageId, parentId, groupId," + " authorId, authorName, authorKey, contentType," + " timestamp, length, bodyStart, bodyLength, raw," + " incoming, read)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," - + " FALSE)"; + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getId().getBytes()); if(m.getParent() == null) ps.setNull(2, BINARY); @@ -682,28 +741,6 @@ abstract class JdbcDatabase implements Database<Connection> { int affected = ps.executeUpdate(); if(affected != 1) throw new DbStateException(); ps.close(); - return true; - } catch(SQLException e) { - tryToClose(ps); - throw new DbException(e); - } - } - - public void addLocalAuthor(Connection txn, LocalAuthor a) - throws DbException { - PreparedStatement ps = null; - try { - String sql = "INSERT INTO localAuthors" - + " (authorId, name, publicKey, privateKey)" - + " VALUES (?, ?, ?, ?)"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, a.getId().getBytes()); - ps.setString(2, a.getName()); - ps.setBytes(3, a.getPublicKey()); - ps.setBytes(4, a.getPrivateKey()); - int affected = ps.executeUpdate(); - if(affected != 1) throw new DbStateException(); - ps.close(); } catch(SQLException e) { tryToClose(ps); throw new DbException(e); @@ -741,40 +778,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean addPrivateMessage(Connection txn, Message m, ContactId c, - boolean incoming) throws DbException { - if(m.getGroup() != null) throw new IllegalArgumentException(); - if(m.getAuthor() != null) throw new IllegalArgumentException(); - if(containsMessage(txn, m.getId())) return false; - PreparedStatement ps = null; - try { - String sql = "INSERT INTO messages (messageId, parentId," - + " contentType, timestamp, length, bodyStart," - + " bodyLength, raw, incoming, contactId, read)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getId().getBytes()); - if(m.getParent() == null) ps.setNull(2, BINARY); - else ps.setBytes(2, m.getParent().getBytes()); - ps.setString(3, m.getContentType()); - ps.setLong(4, m.getTimestamp()); - byte[] raw = m.getSerialised(); - ps.setInt(5, raw.length); - ps.setInt(6, m.getBodyStart()); - ps.setInt(7, m.getBodyLength()); - ps.setBytes(8, raw); - ps.setBoolean(9, incoming); - ps.setInt(10, c.getInt()); - int affected = ps.executeUpdate(); - if(affected != 1) throw new DbStateException(); - ps.close(); - return true; - } catch(SQLException e) { - tryToClose(ps); - throw new DbException(e); - } - } - public void addSecrets(Connection txn, Collection<TemporarySecret> secrets) throws DbException { PreparedStatement ps = null; @@ -841,36 +844,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean addSubscription(Connection txn, Group g) throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT COUNT (groupId) FROM groups"; - ps = txn.prepareStatement(sql); - rs = ps.executeQuery(); - if(!rs.next()) throw new DbStateException(); - int count = rs.getInt(1); - if(rs.next()) throw new DbStateException(); - rs.close(); - ps.close(); - if(count > MAX_SUBSCRIPTIONS) throw new DbStateException(); - if(count == MAX_SUBSCRIPTIONS) return false; - sql = "INSERT INTO groups (groupId, name, salt, visibleToAll)" - + " VALUES (?, ?, ?, FALSE)"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getId().getBytes()); - ps.setString(2, g.getName()); - ps.setBytes(3, g.getSalt()); - int affected = ps.executeUpdate(); - if(affected != 1) throw new DbStateException(); - ps.close(); - return true; - } catch(SQLException e) { - tryToClose(ps); - throw new DbException(e); - } - } - public boolean addTransport(Connection txn, TransportId t, long maxLatency) throws DbException { PreparedStatement ps = null; @@ -931,8 +904,9 @@ abstract class JdbcDatabase implements Database<Connection> { throws DbException { PreparedStatement ps = null; try { - String sql = "INSERT INTO groupVisibilities (contactId, groupId)" - + " VALUES (?, ?)"; + String sql = "INSERT INTO groupVisibilities" + + " (contactId, groupId, inbox)" + + " VALUES (?, ?, FALSE)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); ps.setBytes(2, g.getBytes()); @@ -997,6 +971,27 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public boolean containsGroup(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM groups WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return found; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public boolean containsLocalAuthor(Connection txn, AuthorId a) throws DbException { PreparedStatement ps = null; @@ -1039,14 +1034,29 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean containsSubscription(Connection txn, GroupId g) + public boolean containsSendableMessages(Connection txn, ContactId c) throws DbException { + long now = clock.currentTimeMillis(); PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT NULL FROM groups WHERE groupId = ?"; + String sql = "SELECT NULL FROM messages AS m" + + " JOIN contactGroups AS cg" + + " ON m.groupId = cg.groupId" + + " JOIN groupVisibilities AS gv" + + " ON m.groupId = gv.groupId" + + " AND cg.contactId = gv.contactId" + + " JOIN retentionVersions AS rv" + + " ON cg.contactId = rv.contactId" + + " JOIN statuses AS s" + + " ON m.messageId = s.messageId" + + " AND cg.contactId = s.contactId" + + " WHERE cg.contactId = ?" + + " AND timestamp >= retention" + + " AND seen = FALSE AND s.expiry < ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); + ps.setInt(1, c.getInt()); + ps.setLong(2, now); rs = ps.executeQuery(); boolean found = rs.next(); if(rs.next()) throw new DbStateException(); @@ -1081,7 +1091,7 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean containsVisibleSubscription(Connection txn, ContactId c, + public boolean containsVisibleGroup(Connection txn, ContactId c, GroupId g) throws DbException { PreparedStatement ps = null; ResultSet rs = null; @@ -1110,32 +1120,36 @@ abstract class JdbcDatabase implements Database<Connection> { ResultSet rs = null; try { // Add all subscribed groups to the list - String sql = "SELECT groupId, name, salt, visibleToAll FROM groups"; + String sql = "SELECT groupId, name, salt, private, visibleToAll" + + " FROM groups"; ps = txn.prepareStatement(sql); rs = ps.executeQuery(); List<GroupStatus> groups = new ArrayList<GroupStatus>(); - Set<GroupId> subscribed = new HashSet<GroupId>(); while(rs.next()) { GroupId id = new GroupId(rs.getBytes(1)); - subscribed.add(id); String name = rs.getString(2); byte[] salt = rs.getBytes(3); - Group group = new Group(id, name, salt); - boolean visibleToAll = rs.getBoolean(4); + boolean isPrivate = rs.getBoolean(4); + Group group = new Group(id, name, salt, isPrivate); + boolean visibleToAll = rs.getBoolean(5); groups.add(new GroupStatus(group, true, visibleToAll)); } rs.close(); ps.close(); - // Add all contact groups to the list, unless already added - sql = "SELECT DISTINCT groupId, name, salt FROM contactGroups"; + // Add all unsubscribed groups to the list + sql = "SELECT DISTINCT cg.groupId, cg.name, cg.salt, cg.private" + + " FROM contactGroups AS cg" + + " LEFT OUTER JOIN groups AS g" + + " ON cg.groupId = g.groupId" + + " WHERE g.groupId IS NULL"; ps = txn.prepareStatement(sql); rs = ps.executeQuery(); while(rs.next()) { GroupId id = new GroupId(rs.getBytes(1)); - if(subscribed.contains(id)) continue; String name = rs.getString(2); byte[] salt = rs.getBytes(3); - Group group = new Group(id, name, salt); + boolean isPrivate = rs.getBoolean(4); + Group group = new Group(id, name, salt, isPrivate); groups.add(new GroupStatus(group, false, false)); } rs.close(); @@ -1298,16 +1312,18 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT name, salt FROM groups WHERE groupId = ?"; + String sql = "SELECT name, salt, private FROM groups" + + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); rs = ps.executeQuery(); if(!rs.next()) throw new DbStateException(); String name = rs.getString(1); byte[] salt = rs.getBytes(2); + boolean isPrivate = rs.getBoolean(3); rs.close(); ps.close(); - return new Group(g, name, salt); + return new Group(g, name, salt, isPrivate); } catch(SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1315,43 +1331,24 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<GroupMessageHeader> getGroupMessageHeaders(Connection txn, - GroupId g) throws DbException { + public Collection<Group> getGroups(Connection txn) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT messageId, parentId, authorId, authorName," - + " authorKey, contentType, timestamp, read" - + " FROM messages" - + " WHERE groupId = ?"; + String sql = "SELECT groupId, name, salt, private FROM groups"; ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); rs = ps.executeQuery(); - List<GroupMessageHeader> headers = - new ArrayList<GroupMessageHeader>(); + List<Group> groups = new ArrayList<Group>(); while(rs.next()) { - MessageId id = new MessageId(rs.getBytes(1)); - byte[] b = rs.getBytes(2); - MessageId parent = b == null ? null : new MessageId(b); - Author author; - b = rs.getBytes(3); - if(b == null) { - author = null; - } else { - AuthorId authorId = new AuthorId(b); - String authorName = rs.getString(4); - byte[] authorKey = rs.getBytes(5); - author = new Author(authorId, authorName, authorKey); - } - String contentType = rs.getString(6); - long timestamp = rs.getLong(7); - boolean read = rs.getBoolean(8); - headers.add(new GroupMessageHeader(id, parent, author, - contentType, timestamp, read, g)); + GroupId id = new GroupId(rs.getBytes(1)); + String name = rs.getString(2); + byte[] salt = rs.getBytes(3); + boolean isPrivate = rs.getBoolean(4); + groups.add(new Group(id, name, salt, isPrivate)); } rs.close(); ps.close(); - return Collections.unmodifiableList(headers); + return Collections.unmodifiableList(groups); } catch(SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1359,27 +1356,87 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public MessageId getGroupMessageParent(Connection txn, MessageId m) + public GroupId getInboxGroup(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT m1.parentId FROM messages AS m1" - + " JOIN messages AS m2" - + " ON m1.parentId = m2.messageId" - + " AND m1.groupId = m2.groupId" - + " WHERE m1.messageId = ?"; + String sql = "SELECT groupId FROM groupVisibilities" + + " WHERE contactId = ?" + + " AND inbox = TRUE"; ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); + ps.setInt(1, c.getInt()); rs = ps.executeQuery(); - MessageId parent = null; - if(rs.next()) { - parent = new MessageId(rs.getBytes(1)); - if(rs.next()) throw new DbStateException(); + GroupId inbox = null; + if(rs.next()) inbox = new GroupId(rs.getBytes(1)); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return inbox; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Collection<MessageHeader> getInboxMessageHeaders(Connection txn, + ContactId c) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + // Get the local and remote authors + String sql = "SELECT l.authorId, l.name, l.publicKey," + + " r.authorId, r.name, r.publicKay" + + " FROM localAuthors AS l" + + " JOIN contacts AS r" + + " ON l.authorId = r.localAuthorId" + + " WHERE contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + AuthorId localId = new AuthorId(rs.getBytes(1)); + String localName = rs.getString(2); + byte[] localKey = rs.getBytes(3); + Author localAuthor = new Author(localId, localName, localKey); + AuthorId remoteId = new AuthorId(rs.getBytes(4)); + String remoteName = rs.getString(5); + byte[] remoteKey = rs.getBytes(6); + Author remoteAuthor = new Author(remoteId, remoteName, remoteKey); + // Get the message headers + sql = "SELECT messageId, parentId, m.groupId, contentType," + + " timestamp, incoming, read" + + " FROM messages AS m" + + " JOIN groups AS g" + + " ON m.groupId = g.groupId" + + " JOIN groupVisibilities AS gv" + + " ON m.groupId = gv.groupId" + + " WHERE gv.contactId = ?" + + " AND inbox = TRUE"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + List<MessageHeader> headers = new ArrayList<MessageHeader>(); + while(rs.next()) { + MessageId id = new MessageId(rs.getBytes(1)); + byte[] b = rs.getBytes(2); + MessageId parent = b == null ? null : new MessageId(b); + GroupId groupId = new GroupId(rs.getBytes(3)); + String contentType = rs.getString(4); + long timestamp = rs.getLong(5); + boolean incoming = rs.getBoolean(6); + boolean read = rs.getBoolean(7); + if(incoming) { + headers.add(new MessageHeader(id, parent, groupId, + remoteAuthor, contentType, timestamp, read)); + } else { + headers.add(new MessageHeader(id, parent, groupId, + localAuthor, contentType, timestamp, read)); + } } rs.close(); ps.close(); - return parent; + return Collections.unmodifiableList(headers); } catch(SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1542,22 +1599,65 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c, - int maxMessages) throws DbException { + public Collection<MessageHeader> getMessageHeaders(Connection txn, + GroupId g) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT messageId FROM messagesToAck" - + " WHERE contactId = ?" - + " LIMIT ?"; + String sql = "SELECT messageId, parentId, authorId, authorName," + + " authorKey, contentType, timestamp, read" + + " FROM messages" + + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setInt(2, maxMessages); + ps.setBytes(1, g.getBytes()); rs = ps.executeQuery(); - List<MessageId> ids = new ArrayList<MessageId>(); - while(rs.next()) ids.add(new MessageId(rs.getBytes(1))); - rs.close(); - ps.close(); + List<MessageHeader> headers = new ArrayList<MessageHeader>(); + while(rs.next()) { + MessageId id = new MessageId(rs.getBytes(1)); + byte[] b = rs.getBytes(2); + MessageId parent = b == null ? null : new MessageId(b); + Author author; + b = rs.getBytes(3); + if(b == null) { + author = null; + } else { + AuthorId authorId = new AuthorId(b); + String authorName = rs.getString(4); + byte[] authorKey = rs.getBytes(5); + author = new Author(authorId, authorName, authorKey); + } + String contentType = rs.getString(6); + long timestamp = rs.getLong(7); + boolean read = rs.getBoolean(8); + headers.add(new MessageHeader(id, parent, g, author, + contentType, timestamp, read)); + } + rs.close(); + ps.close(); + return Collections.unmodifiableList(headers); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c, + int maxMessages) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM messagesToAck" + + " WHERE contactId = ?" + + " LIMIT ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setInt(2, maxMessages); + rs = ps.executeQuery(); + List<MessageId> ids = new ArrayList<MessageId>(); + while(rs.next()) ids.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); return Collections.unmodifiableList(ids); } catch(SQLException e) { tryToClose(rs); @@ -1648,72 +1748,26 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<PrivateMessageHeader> getPrivateMessageHeaders( - Connection txn, ContactId c) throws DbException { + public MessageId getParent(Connection txn, MessageId m) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - // Get the incoming message headers - String sql = "SELECT m.messageId, parentId, contentType," - + " timestamp, read, c.authorId, name, publicKey" - + " FROM messages AS m" - + " JOIN contacts AS c" - + " ON m.contactId = c.contactId" - + " WHERE m.contactId = ?" - + " AND groupId IS NULL" - + " AND incoming = TRUE"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - rs = ps.executeQuery(); - List<PrivateMessageHeader> headers = - new ArrayList<PrivateMessageHeader>(); - while(rs.next()) { - MessageId id = new MessageId(rs.getBytes(1)); - byte[] b = rs.getBytes(2); - MessageId parent = b == null ? null : new MessageId(b); - String contentType = rs.getString(3); - long timestamp = rs.getLong(4); - boolean read = rs.getBoolean(5); - AuthorId authorId = new AuthorId(rs.getBytes(6)); - String authorName = rs.getString(7); - byte[] authorKey = rs.getBytes(8); - Author author = new Author(authorId, authorName, authorKey); - headers.add(new PrivateMessageHeader(id, parent, author, - contentType, timestamp, read, c, true)); - } - rs.close(); - ps.close(); - // Get the outgoing message headers - sql = "SELECT m.messageId, parentId, contentType, timestamp, read," - + " a.authorId, a.name, a.publicKey" - + " FROM messages AS m" - + " JOIN contacts AS c" - + " ON m.contactId = c.contactId" - + " JOIN localAuthors AS a" - + " ON c.localAuthorId = a.authorId" - + " WHERE m.contactId = ?" - + " AND groupId IS NULL" - + " AND incoming = FALSE"; + String sql = "SELECT m1.parentId FROM messages AS m1" + + " JOIN messages AS m2" + + " ON m1.parentId = m2.messageId" + + " AND m1.groupId = m2.groupId" + + " WHERE m1.messageId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); + ps.setBytes(1, m.getBytes()); rs = ps.executeQuery(); - while(rs.next()) { - MessageId id = new MessageId(rs.getBytes(1)); - byte[] b = rs.getBytes(2); - MessageId parent = b == null ? null : new MessageId(b); - String contentType = rs.getString(3); - long timestamp = rs.getLong(4); - boolean read = rs.getBoolean(5); - AuthorId authorId = new AuthorId(rs.getBytes(6)); - String authorName = rs.getString(7); - byte[] authorKey = rs.getBytes(8); - Author author = new Author(authorId, authorName, authorKey); - headers.add(new PrivateMessageHeader(id, parent, author, - contentType, timestamp, read, c, false)); + MessageId parent = null; + if(rs.next()) { + parent = new MessageId(rs.getBytes(1)); + if(rs.next()) throw new DbStateException(); } rs.close(); ps.close(); - return Collections.unmodifiableList(headers); + return parent; } catch(SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1751,29 +1805,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - // Do we have a sendable private message with the given ID? String sql = "SELECT length, raw FROM messages AS m" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " WHERE m.messageId = ? AND m.contactId = ?" - + " AND seen = FALSE AND expiry < ?"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); - ps.setInt(2, c.getInt()); - ps.setLong(3, now); - rs = ps.executeQuery(); - byte[] raw = null; - if(rs.next()) { - int length = rs.getInt(1); - raw = rs.getBlob(2).getBytes(1, length); - if(raw.length != length) throw new DbStateException(); - } - if(rs.next()) throw new DbStateException(); - rs.close(); - ps.close(); - if(raw != null) return raw; - // Do we have a sendable group message with the given ID? - sql = "SELECT length, raw FROM messages AS m" + " JOIN contactGroups AS cg" + " ON m.groupId = cg.groupId" + " JOIN groupVisibilities AS gv" @@ -1793,6 +1825,7 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setInt(2, c.getInt()); ps.setLong(3, now); rs = ps.executeQuery(); + byte[] raw = null; if(rs.next()) { int length = rs.getInt(1); raw = rs.getBlob(2).getBytes(1, length); @@ -1989,29 +2022,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - // Do we have any sendable private messages? String sql = "SELECT length, m.messageId FROM messages AS m" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " WHERE m.contactId = ? AND seen = FALSE AND expiry < ?" - + " ORDER BY timestamp DESC"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setLong(2, now); - rs = ps.executeQuery(); - List<MessageId> ids = new ArrayList<MessageId>(); - int total = 0; - while(rs.next()) { - int length = rs.getInt(1); - if(total + length > maxLength) break; - ids.add(new MessageId(rs.getBytes(2))); - total += length; - } - rs.close(); - ps.close(); - if(total == maxLength) return Collections.unmodifiableList(ids); - // Do we have any sendable group messages? - sql = "SELECT length, m.messageId FROM messages AS m" + " JOIN contactGroups AS cg" + " ON m.groupId = cg.groupId" + " JOIN groupVisibilities AS gv" @@ -2030,6 +2041,8 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setInt(1, c.getInt()); ps.setLong(2, now); rs = ps.executeQuery(); + List<MessageId> ids = new ArrayList<MessageId>(); + int total = 0; while(rs.next()) { int length = rs.getInt(1); if(total + length > maxLength) break; @@ -2046,58 +2059,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<Group> getSubscriptions(Connection txn) - throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT groupId, name, salt FROM groups"; - ps = txn.prepareStatement(sql); - rs = ps.executeQuery(); - List<Group> subs = new ArrayList<Group>(); - while(rs.next()) { - GroupId groupId = new GroupId(rs.getBytes(1)); - String name = rs.getString(2); - byte[] salt = rs.getBytes(3); - subs.add(new Group(groupId, name, salt)); - } - rs.close(); - ps.close(); - return Collections.unmodifiableList(subs); - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - - public Collection<Group> getSubscriptions(Connection txn, ContactId c) - throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT groupId, name, salt FROM contactGroups" - + " WHERE contactId = ?"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - rs = ps.executeQuery(); - List<Group> subs = new ArrayList<Group>(); - while(rs.next()) { - GroupId groupId = new GroupId(rs.getBytes(1)); - String name = rs.getString(2); - byte[] salt = rs.getBytes(3); - subs.add(new Group(groupId, name, salt)); - } - rs.close(); - ps.close(); - return Collections.unmodifiableList(subs); - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public SubscriptionAck getSubscriptionAck(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; @@ -2138,7 +2099,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT g.groupId, name, salt," + String sql = "SELECT g.groupId, name, salt, private," + " localVersion, txCount" + " FROM groups AS g" + " JOIN groupVisibilities AS vis" @@ -2152,20 +2113,21 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setInt(1, c.getInt()); ps.setLong(2, now); rs = ps.executeQuery(); - List<Group> subs = new ArrayList<Group>(); + List<Group> groups = new ArrayList<Group>(); long version = 0; int txCount = 0; while(rs.next()) { - GroupId groupId = new GroupId(rs.getBytes(1)); + GroupId id = new GroupId(rs.getBytes(1)); String name = rs.getString(2); byte[] salt = rs.getBytes(3); - subs.add(new Group(groupId, name, salt)); - version = rs.getLong(4); - txCount = rs.getInt(5); + boolean isPrivate = rs.getBoolean(4); + groups.add(new Group(id, name, salt, isPrivate)); + version = rs.getLong(5); + txCount = rs.getInt(6); } rs.close(); ps.close(); - if(subs.isEmpty()) return null; + if(groups.isEmpty()) return null; sql = "UPDATE groupVersions" + " SET expiry = ?, txCount = txCount + 1" + " WHERE contactId = ?"; @@ -2175,8 +2137,8 @@ abstract class JdbcDatabase implements Database<Connection> { int affected = ps.executeUpdate(); if(affected != 1) throw new DbStateException(); ps.close(); - subs = Collections.unmodifiableList(subs); - return new SubscriptionUpdate(subs, version); + groups = Collections.unmodifiableList(groups); + return new SubscriptionUpdate(groups, version); } catch(SQLException e) { tryToClose(ps); tryToClose(rs); @@ -2347,7 +2309,6 @@ abstract class JdbcDatabase implements Database<Connection> { String sql = "SELECT groupId, COUNT(*)" + " FROM messages AS m" + " WHERE read = FALSE" - + " AND groupId IS NOT NULL" + " GROUP BY groupId"; ps = txn.prepareStatement(sql); rs = ps.executeQuery(); @@ -2388,13 +2349,17 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<GroupId> getVisibleSubscriptions(Connection txn, + public Collection<GroupId> getVisiblePrivateGroups(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT groupId FROM groupVisibilities" - + " WHERE contactId = ?"; + String sql = "SELECT g.groupId" + + " FROM groups AS g" + + " JOIN groupVisibilities AS gv" + + " ON g.groupId = gv.groupId" + + " WHERE contactId = ?" + + " AND private = TRUE"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); rs = ps.executeQuery(); @@ -2410,59 +2375,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean hasSendableMessages(Connection txn, ContactId c) - throws DbException { - long now = clock.currentTimeMillis(); - PreparedStatement ps = null; - ResultSet rs = null; - try { - // Do we have any sendable private messages? - String sql = "SELECT m.messageId FROM messages AS m" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " WHERE m.contactId = ? AND seen = FALSE AND expiry < ?" - + " LIMIT 1"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setLong(2, now); - rs = ps.executeQuery(); - boolean found = rs.next(); - if(rs.next()) throw new DbStateException(); - rs.close(); - ps.close(); - if(found) return true; - // Do we have any sendable group messages? - sql = "SELECT m.messageId FROM messages AS m" - + " JOIN contactGroups AS cg" - + " ON m.groupId = cg.groupId" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " AND cg.contactId = gv.contactId" - + " JOIN retentionVersions AS rv" - + " ON cg.contactId = rv.contactId" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND cg.contactId = s.contactId" - + " WHERE cg.contactId = ?" - + " AND timestamp >= retention" - + " AND seen = FALSE AND s.expiry < ?" - + " LIMIT 1"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setLong(2, now); - rs = ps.executeQuery(); - found = rs.next(); - if(rs.next()) throw new DbStateException(); - rs.close(); - ps.close(); - return found; - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public long incrementConnectionCounter(Connection txn, ContactId c, TransportId t, long period) throws DbException { PreparedStatement ps = null; @@ -2516,25 +2428,25 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void removeOutstandingMessages(Connection txn, ContactId c, - Collection<MessageId> acked) throws DbException { + public void mergeConfig(Connection txn, TransportId t, TransportConfig c) + throws DbException { + // Merge the new configuration with the existing one + mergeStringMap(txn, t, c, "transportConfigs"); + } + + public void mergeLocalProperties(Connection txn, TransportId t, + TransportProperties p) throws DbException { + // Merge the new properties with the existing ones + mergeStringMap(txn, t, p, "transportProperties"); + // Bump the transport version PreparedStatement ps = null; try { - // Set the status of each message to seen = true - String sql = "UPDATE statuses SET seen = TRUE" - + " WHERE messageId = ? AND contactId = ?"; + String sql = "UPDATE transportVersions" + + " SET localVersion = localVersion + 1, expiry = 0" + + " WHERE transportId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - for(MessageId m : acked) { - ps.setBytes(2, m.getBytes()); - ps.addBatch(); - } - int[] batchAffected = ps.executeBatch(); - if(batchAffected.length != acked.size()) - throw new DbStateException(); - for(int i = 0; i < batchAffected.length; i++) { - if(batchAffected[i] > 1) throw new DbStateException(); - } + ps.setBytes(1, t.getBytes()); + ps.executeUpdate(); ps.close(); } catch(SQLException e) { tryToClose(ps); @@ -2542,21 +2454,42 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void removeMessagesToAck(Connection txn, ContactId c, - Collection<MessageId> acked) throws DbException { + private void mergeStringMap(Connection txn, TransportId t, + Map<String, String> m, String tableName) throws DbException { PreparedStatement ps = null; try { - String sql = "DELETE FROM messagesToAck" - + " WHERE contactId = ? AND messageId = ?"; + // Update any properties that already exist + String sql = "UPDATE " + tableName + " SET value = ?" + + " WHERE transportId = ? AND key = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - for(MessageId m : acked) { - ps.setBytes(2, m.getBytes()); + ps.setBytes(2, t.getBytes()); + for(Entry<String, String> e : m.entrySet()) { + ps.setString(1, e.getValue()); + ps.setString(3, e.getKey()); ps.addBatch(); } int[] batchAffected = ps.executeBatch(); - if(batchAffected.length != acked.size()) - throw new DbStateException(); + if(batchAffected.length != m.size()) throw new DbStateException(); + for(int i = 0; i < batchAffected.length; i++) { + if(batchAffected[i] > 1) throw new DbStateException(); + } + // Insert any properties that don't already exist + sql = "INSERT INTO " + tableName + " (transportId, key, value)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, t.getBytes()); + int updateIndex = 0, inserted = 0; + for(Entry<String, String> e : m.entrySet()) { + if(batchAffected[updateIndex] == 0) { + ps.setString(2, e.getKey()); + ps.setString(3, e.getValue()); + ps.addBatch(); + inserted++; + } + updateIndex++; + } + batchAffected = ps.executeBatch(); + if(batchAffected.length != inserted) throw new DbStateException(); for(int i = 0; i < batchAffected.length; i++) { if(batchAffected[i] != 1) throw new DbStateException(); } @@ -2583,6 +2516,51 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void removeGroup(Connection txn, GroupId g) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + // Find out which contacts are affected + String sql = "SELECT contactId FROM groupVisibilities" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + Collection<Integer> visible = new ArrayList<Integer>(); + while(rs.next()) visible.add(rs.getInt(1)); + rs.close(); + ps.close(); + // Delete the group + sql = "DELETE FROM groups WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + if(visible.isEmpty()) return; + // Bump the subscription versions for the affected contacts + sql = "UPDATE groupVersions" + + " SET localVersion = localVersion + 1, expiry = 0" + + " WHERE contactId = ?"; + ps = txn.prepareStatement(sql); + for(Integer c : visible) { + ps.setInt(1, c); + ps.addBatch(); + } + int[] batchAffected = ps.executeBatch(); + if(batchAffected.length != visible.size()) + throw new DbStateException(); + for(int i = 0; i < batchAffected.length; i++) { + if(batchAffected[i] != 1) throw new DbStateException(); + } + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + tryToClose(rs); + throw new DbException(e); + } + } + public void removeLocalAuthor(Connection txn, AuthorId a) throws DbException { PreparedStatement ps = null; @@ -2614,40 +2592,20 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void removeSubscription(Connection txn, GroupId g) - throws DbException { + public void removeMessagesToAck(Connection txn, ContactId c, + Collection<MessageId> acked) throws DbException { PreparedStatement ps = null; - ResultSet rs = null; try { - // Find out which contacts are affected - String sql = "SELECT contactId FROM groupVisibilities" - + " WHERE groupId = ?"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); - rs = ps.executeQuery(); - Collection<Integer> visible = new ArrayList<Integer>(); - while(rs.next()) visible.add(rs.getInt(1)); - rs.close(); - ps.close(); - // Delete the group - sql = "DELETE FROM groups WHERE groupId = ?"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); - int affected = ps.executeUpdate(); - if(affected != 1) throw new DbStateException(); - ps.close(); - if(visible.isEmpty()) return; - // Bump the subscription version for the affected contacts - sql = "UPDATE groupVersions" - + " SET localVersion = localVersion + 1, expiry = 0" - + " WHERE contactId = ?"; + String sql = "DELETE FROM messagesToAck" + + " WHERE contactId = ? AND messageId = ?"; ps = txn.prepareStatement(sql); - for(Integer c : visible) { - ps.setInt(1, c); + ps.setInt(1, c.getInt()); + for(MessageId m : acked) { + ps.setBytes(2, m.getBytes()); ps.addBatch(); } int[] batchAffected = ps.executeBatch(); - if(batchAffected.length != visible.size()) + if(batchAffected.length != acked.size()) throw new DbStateException(); for(int i = 0; i < batchAffected.length; i++) { if(batchAffected[i] != 1) throw new DbStateException(); @@ -2655,7 +2613,6 @@ abstract class JdbcDatabase implements Database<Connection> { ps.close(); } catch(SQLException e) { tryToClose(ps); - tryToClose(rs); throw new DbException(e); } } @@ -2703,25 +2660,20 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void mergeConfig(Connection txn, TransportId t, TransportConfig c) - throws DbException { - // Merge the new configuration with the existing one - mergeStringMap(txn, t, c, "transportConfigs"); - } - - public void mergeLocalProperties(Connection txn, TransportId t, - TransportProperties p) throws DbException { - // Merge the new properties with the existing ones - mergeStringMap(txn, t, p, "transportProperties"); - // Bump the transport version + public void setConnectionWindow(Connection txn, ContactId c, TransportId t, + long period, long centre, byte[] bitmap) throws DbException { PreparedStatement ps = null; try { - String sql = "UPDATE transportVersions" - + " SET localVersion = localVersion + 1, expiry = 0" - + " WHERE transportId = ?"; + String sql = "UPDATE secrets SET centre = ?, bitmap = ?" + + " WHERE contactId = ? AND transportId = ? AND period = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, t.getBytes()); - ps.executeUpdate(); + ps.setLong(1, centre); + ps.setBytes(2, bitmap); + ps.setInt(3, c.getInt()); + ps.setBytes(4, t.getBytes()); + ps.setLong(5, period); + int affected = ps.executeUpdate(); + if(affected > 1) throw new DbStateException(); ps.close(); } catch(SQLException e) { tryToClose(ps); @@ -2729,66 +2681,69 @@ abstract class JdbcDatabase implements Database<Connection> { } } - private void mergeStringMap(Connection txn, TransportId t, - Map<String, String> m, String tableName) throws DbException { + public boolean setGroups(Connection txn, ContactId c, + Collection<Group> groups, long version) throws DbException { PreparedStatement ps = null; + ResultSet rs = null; try { - // Update any properties that already exist - String sql = "UPDATE " + tableName + " SET value = ?" - + " WHERE transportId = ? AND key = ?"; + // Mark the update as needing to be acked + String sql = "UPDATE groupVersions" + + " SET remoteVersion = ?, remoteAcked = FALSE" + + " WHERE contactId = ? AND remoteVersion < ?"; ps = txn.prepareStatement(sql); - ps.setBytes(2, t.getBytes()); - for(Entry<String, String> e : m.entrySet()) { - ps.setString(1, e.getValue()); - ps.setString(3, e.getKey()); + ps.setLong(1, version); + ps.setInt(2, c.getInt()); + ps.setLong(3, version); + int affected = ps.executeUpdate(); + if(affected > 1) throw new DbStateException(); + ps.close(); + // Return false if the update is obsolete + if(affected == 0) return false; + // Delete the existing subscriptions, if any + sql = "DELETE FROM contactGroups WHERE contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.executeUpdate(); + // Store the new subscriptions, if any + if(groups.isEmpty()) return true; + sql = "INSERT INTO contactGroups" + + " (contactId, groupId, name, salt, private)" + + " VALUES (?, ?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + for(Group g : groups) { + ps.setBytes(2, g.getId().getBytes()); + ps.setString(3, g.getName()); + ps.setBytes(4, g.getSalt()); + ps.setBoolean(5, g.isPrivate()); ps.addBatch(); } int[] batchAffected = ps.executeBatch(); - if(batchAffected.length != m.size()) throw new DbStateException(); - for(int i = 0; i < batchAffected.length; i++) { - if(batchAffected[i] > 1) throw new DbStateException(); - } - // Insert any properties that don't already exist - sql = "INSERT INTO " + tableName + " (transportId, key, value)" - + " VALUES (?, ?, ?)"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, t.getBytes()); - int updateIndex = 0, inserted = 0; - for(Entry<String, String> e : m.entrySet()) { - if(batchAffected[updateIndex] == 0) { - ps.setString(2, e.getKey()); - ps.setString(3, e.getValue()); - ps.addBatch(); - inserted++; - } - updateIndex++; - } - batchAffected = ps.executeBatch(); - if(batchAffected.length != inserted) throw new DbStateException(); + if(batchAffected.length != groups.size()) + throw new DbStateException(); for(int i = 0; i < batchAffected.length; i++) { if(batchAffected[i] != 1) throw new DbStateException(); } ps.close(); + return true; } catch(SQLException e) { tryToClose(ps); + tryToClose(rs); throw new DbException(e); } } - public void setConnectionWindow(Connection txn, ContactId c, TransportId t, - long period, long centre, byte[] bitmap) throws DbException { + public void setLastConnected(Connection txn, ContactId c, long now) + throws DbException { PreparedStatement ps = null; try { - String sql = "UPDATE secrets SET centre = ?, bitmap = ?" - + " WHERE contactId = ? AND transportId = ? AND period = ?"; + String sql = "UPDATE connectionTimes SET lastConnected = ?" + + " WHERE contactId = ?"; ps = txn.prepareStatement(sql); - ps.setLong(1, centre); - ps.setBytes(2, bitmap); - ps.setInt(3, c.getInt()); - ps.setBytes(4, t.getBytes()); - ps.setLong(5, period); + ps.setLong(1, now); + ps.setInt(2, c.getInt()); int affected = ps.executeUpdate(); - if(affected > 1) throw new DbStateException(); + if(affected < 1) throw new DbStateException(); ps.close(); } catch(SQLException e) { tryToClose(ps); @@ -2796,17 +2751,43 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void setLastConnected(Connection txn, ContactId c, long now) + public void setInboxGroup(Connection txn, ContactId c, Group g) throws DbException { + if(!g.isPrivate()) throw new IllegalArgumentException(); PreparedStatement ps = null; try { - String sql = "UPDATE connectionTimes SET lastConnected = ?" + // Unset any existing inbox group for the contact + String sql = "UPDATE groupVisibilities" + + " SET inbox = FALSE" + " WHERE contactId = ?"; ps = txn.prepareStatement(sql); - ps.setLong(1, now); - ps.setInt(2, c.getInt()); + ps.setInt(1, c.getInt()); + ps.executeUpdate(); int affected = ps.executeUpdate(); - if(affected < 1) throw new DbStateException(); + if(affected > 1) throw new DbStateException(); + ps.close(); + // Make the group visible to the contact and set it as the inbox + sql = "INSERT INTO groupVisibilities" + + " (contactId, groupId, inbox)" + + " VALUES (?, ?, TRUE)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setBytes(2, g.getId().getBytes()); + affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + // Add the group to the contact's subscriptions + sql = "INSERT INTO contactGroups" + + " (contactId, groupId, name, salt, private)" + + " VALUES (?, ?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setBytes(2, g.getId().getBytes()); + ps.setString(3, g.getName()); + ps.setBytes(4, g.getSalt()); + ps.setBoolean(5, g.isPrivate()); + affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); ps.close(); } catch(SQLException e) { tryToClose(ps); @@ -3050,57 +3031,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean setSubscriptions(Connection txn, ContactId c, - Collection<Group> subs, long version) throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - // Mark the update as needing to be acked - String sql = "UPDATE groupVersions" - + " SET remoteVersion = ?, remoteAcked = FALSE" - + " WHERE contactId = ? AND remoteVersion < ?"; - ps = txn.prepareStatement(sql); - ps.setLong(1, version); - ps.setInt(2, c.getInt()); - ps.setLong(3, version); - int affected = ps.executeUpdate(); - if(affected > 1) throw new DbStateException(); - ps.close(); - // Return false if the update is obsolete - if(affected == 0) return false; - // Delete the existing subscriptions, if any - sql = "DELETE FROM contactGroups WHERE contactId = ?"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.executeUpdate(); - // Store the new subscriptions, if any - if(subs.isEmpty()) return true; - sql = "INSERT INTO contactGroups" - + " (contactId, groupId, name, salt)" - + " VALUES (?, ?, ?, ?)"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - for(Group g : subs) { - ps.setBytes(2, g.getId().getBytes()); - ps.setString(3, g.getName()); - ps.setBytes(4, g.getSalt()); - ps.addBatch(); - } - int[] batchAffected = ps.executeBatch(); - if(batchAffected.length != subs.size()) - throw new DbStateException(); - for(int i = 0; i < batchAffected.length; i++) { - if(batchAffected[i] != 1) throw new DbStateException(); - } - ps.close(); - return true; - } catch(SQLException e) { - tryToClose(ps); - tryToClose(rs); - throw new DbException(e); - } - } - public void setSubscriptionUpdateAcked(Connection txn, ContactId c, long version) throws DbException { PreparedStatement ps = null; diff --git a/briar-core/src/net/sf/briar/invitation/AliceConnector.java b/briar-core/src/net/sf/briar/invitation/AliceConnector.java index 84cab4286e..3fe1c54917 100644 --- a/briar-core/src/net/sf/briar/invitation/AliceConnector.java +++ b/briar-core/src/net/sf/briar/invitation/AliceConnector.java @@ -21,6 +21,7 @@ import net.sf.briar.api.crypto.KeyManager; import net.sf.briar.api.crypto.PseudoRandom; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.messaging.GroupFactory; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; import net.sf.briar.api.serial.Reader; @@ -43,15 +44,16 @@ class AliceConnector extends Connector { ReaderFactory readerFactory, WriterFactory writerFactory, ConnectionReaderFactory connectionReaderFactory, ConnectionWriterFactory connectionWriterFactory, - AuthorFactory authorFactory, KeyManager keyManager, - ConnectionDispatcher connectionDispatcher, Clock clock, - ConnectorGroup group, DuplexPlugin plugin, LocalAuthor localAuthor, + AuthorFactory authorFactory, GroupFactory groupFactory, + KeyManager keyManager, ConnectionDispatcher connectionDispatcher, + Clock clock, ConnectorGroup group, DuplexPlugin plugin, + LocalAuthor localAuthor, Map<TransportId, TransportProperties> localProps, PseudoRandom random) { super(crypto, db, readerFactory, writerFactory, connectionReaderFactory, - connectionWriterFactory, authorFactory, keyManager, - connectionDispatcher, clock, group, plugin, localAuthor, - localProps, random); + connectionWriterFactory, authorFactory, groupFactory, + keyManager, connectionDispatcher, clock, group, plugin, + localAuthor, localProps, random); } @Override diff --git a/briar-core/src/net/sf/briar/invitation/BobConnector.java b/briar-core/src/net/sf/briar/invitation/BobConnector.java index ae7c02144c..4d05f92f7c 100644 --- a/briar-core/src/net/sf/briar/invitation/BobConnector.java +++ b/briar-core/src/net/sf/briar/invitation/BobConnector.java @@ -21,6 +21,7 @@ import net.sf.briar.api.crypto.KeyManager; import net.sf.briar.api.crypto.PseudoRandom; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.messaging.GroupFactory; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; import net.sf.briar.api.serial.Reader; @@ -43,15 +44,16 @@ class BobConnector extends Connector { ReaderFactory readerFactory, WriterFactory writerFactory, ConnectionReaderFactory connectionReaderFactory, ConnectionWriterFactory connectionWriterFactory, - AuthorFactory authorFactory, KeyManager keyManager, - ConnectionDispatcher connectionDispatcher, Clock clock, - ConnectorGroup group, DuplexPlugin plugin, LocalAuthor localAuthor, + AuthorFactory authorFactory, GroupFactory groupFactory, + KeyManager keyManager, ConnectionDispatcher connectionDispatcher, + Clock clock, ConnectorGroup group, DuplexPlugin plugin, + LocalAuthor localAuthor, Map<TransportId, TransportProperties> localProps, PseudoRandom random) { super(crypto, db, readerFactory, writerFactory, connectionReaderFactory, - connectionWriterFactory, authorFactory, keyManager, - connectionDispatcher, clock, group, plugin, localAuthor, - localProps, random); + connectionWriterFactory, authorFactory, groupFactory, + keyManager, connectionDispatcher, clock, group, plugin, + localAuthor, localProps, random); } @Override diff --git a/briar-core/src/net/sf/briar/invitation/Connector.java b/briar-core/src/net/sf/briar/invitation/Connector.java index 5cceedb6cb..5d22468788 100644 --- a/briar-core/src/net/sf/briar/invitation/Connector.java +++ b/briar-core/src/net/sf/briar/invitation/Connector.java @@ -41,6 +41,8 @@ import net.sf.briar.api.crypto.Signature; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.NoSuchTransportException; +import net.sf.briar.api.messaging.Group; +import net.sf.briar.api.messaging.GroupFactory; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; import net.sf.briar.api.serial.Reader; @@ -64,6 +66,7 @@ abstract class Connector extends Thread { protected final ConnectionReaderFactory connectionReaderFactory; protected final ConnectionWriterFactory connectionWriterFactory; protected final AuthorFactory authorFactory; + protected final GroupFactory groupFactory; protected final KeyManager keyManager; protected final ConnectionDispatcher connectionDispatcher; protected final Clock clock; @@ -84,9 +87,10 @@ abstract class Connector extends Thread { ReaderFactory readerFactory, WriterFactory writerFactory, ConnectionReaderFactory connectionReaderFactory, ConnectionWriterFactory connectionWriterFactory, - AuthorFactory authorFactory, KeyManager keyManager, - ConnectionDispatcher connectionDispatcher, Clock clock, - ConnectorGroup group, DuplexPlugin plugin, LocalAuthor localAuthor, + AuthorFactory authorFactory, GroupFactory groupFactory, + KeyManager keyManager, ConnectionDispatcher connectionDispatcher, + Clock clock, ConnectorGroup group, DuplexPlugin plugin, + LocalAuthor localAuthor, Map<TransportId, TransportProperties> localProps, PseudoRandom random) { super("Connector"); @@ -97,6 +101,7 @@ abstract class Connector extends Thread { this.connectionReaderFactory = connectionReaderFactory; this.connectionWriterFactory = connectionWriterFactory; this.authorFactory = authorFactory; + this.groupFactory = groupFactory; this.keyManager = keyManager; this.connectionDispatcher = connectionDispatcher; this.clock = clock; @@ -267,6 +272,11 @@ abstract class Connector extends Thread { long epoch, boolean alice) throws DbException { // Add the contact to the database contactId = db.addContact(remoteAuthor, localAuthor.getId()); + // Create and store the inbox group + byte[] salt = crypto.deriveGroupSalt(secret); + Group inbox = groupFactory.createGroup("Inbox", salt, true); + db.addGroup(inbox); + db.setInboxGroup(contactId, inbox); // Store the remote transport properties db.setRemoteProperties(contactId, remoteProps); // Create an endpoint for each transport shared with the contact diff --git a/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java b/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java index ee67e4ccd3..c196597524 100644 --- a/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java +++ b/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java @@ -27,6 +27,7 @@ import net.sf.briar.api.db.DbException; import net.sf.briar.api.invitation.InvitationListener; import net.sf.briar.api.invitation.InvitationState; import net.sf.briar.api.invitation.InvitationTask; +import net.sf.briar.api.messaging.GroupFactory; import net.sf.briar.api.plugins.PluginManager; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.serial.ReaderFactory; @@ -48,6 +49,7 @@ class ConnectorGroup extends Thread implements InvitationTask { private final ConnectionReaderFactory connectionReaderFactory; private final ConnectionWriterFactory connectionWriterFactory; private final AuthorFactory authorFactory; + private final GroupFactory groupFactory; private final KeyManager keyManager; private final ConnectionDispatcher connectionDispatcher; private final Clock clock; @@ -74,9 +76,9 @@ class ConnectorGroup extends Thread implements InvitationTask { ReaderFactory readerFactory, WriterFactory writerFactory, ConnectionReaderFactory connectionReaderFactory, ConnectionWriterFactory connectionWriterFactory, - AuthorFactory authorFactory, KeyManager keyManager, - ConnectionDispatcher connectionDispatcher, Clock clock, - PluginManager pluginManager, AuthorId localAuthorId, + AuthorFactory authorFactory, GroupFactory groupFactory, + KeyManager keyManager, ConnectionDispatcher connectionDispatcher, + Clock clock, PluginManager pluginManager, AuthorId localAuthorId, int localInvitationCode, int remoteInvitationCode) { super("ConnectorGroup"); this.crypto = crypto; @@ -86,6 +88,7 @@ class ConnectorGroup extends Thread implements InvitationTask { this.connectionReaderFactory = connectionReaderFactory; this.connectionWriterFactory = connectionWriterFactory; this.authorFactory = authorFactory; + this.groupFactory = groupFactory; this.keyManager = keyManager; this.connectionDispatcher = connectionDispatcher; this.clock = clock; @@ -171,8 +174,8 @@ class ConnectorGroup extends Thread implements InvitationTask { remoteInvitationCode); return new AliceConnector(crypto, db, readerFactory, writerFactory, connectionReaderFactory, connectionWriterFactory, authorFactory, - keyManager, connectionDispatcher, clock, this, plugin, - localAuthor, localProps, random); + groupFactory, keyManager, connectionDispatcher, clock, this, + plugin, localAuthor, localProps, random); } private Connector createBobConnector(DuplexPlugin plugin, @@ -182,8 +185,8 @@ class ConnectorGroup extends Thread implements InvitationTask { localInvitationCode); return new BobConnector(crypto, db, readerFactory, writerFactory, connectionReaderFactory, connectionWriterFactory, authorFactory, - keyManager, connectionDispatcher, clock, this, plugin, - localAuthor, localProps, random); + groupFactory, keyManager, connectionDispatcher, clock, this, + plugin, localAuthor, localProps, random); } public void localConfirmationSucceeded() { diff --git a/briar-core/src/net/sf/briar/invitation/InvitationTaskFactoryImpl.java b/briar-core/src/net/sf/briar/invitation/InvitationTaskFactoryImpl.java index b3fd50162e..b798adc75e 100644 --- a/briar-core/src/net/sf/briar/invitation/InvitationTaskFactoryImpl.java +++ b/briar-core/src/net/sf/briar/invitation/InvitationTaskFactoryImpl.java @@ -10,6 +10,7 @@ import net.sf.briar.api.crypto.KeyManager; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.invitation.InvitationTask; import net.sf.briar.api.invitation.InvitationTaskFactory; +import net.sf.briar.api.messaging.GroupFactory; import net.sf.briar.api.plugins.PluginManager; import net.sf.briar.api.serial.ReaderFactory; import net.sf.briar.api.serial.WriterFactory; @@ -26,6 +27,7 @@ class InvitationTaskFactoryImpl implements InvitationTaskFactory { private final ConnectionReaderFactory connectionReaderFactory; private final ConnectionWriterFactory connectionWriterFactory; private final AuthorFactory authorFactory; + private final GroupFactory groupFactory; private final KeyManager keyManager; private final ConnectionDispatcher connectionDispatcher; private final Clock clock; @@ -36,9 +38,9 @@ class InvitationTaskFactoryImpl implements InvitationTaskFactory { ReaderFactory readerFactory, WriterFactory writerFactory, ConnectionReaderFactory connectionReaderFactory, ConnectionWriterFactory connectionWriterFactory, - AuthorFactory authorFactory, KeyManager keyManager, - ConnectionDispatcher connectionDispatcher, Clock clock, - PluginManager pluginManager) { + AuthorFactory authorFactory, GroupFactory groupFactory, + KeyManager keyManager, ConnectionDispatcher connectionDispatcher, + Clock clock, PluginManager pluginManager) { this.crypto = crypto; this.db = db; this.readerFactory = readerFactory; @@ -46,6 +48,7 @@ class InvitationTaskFactoryImpl implements InvitationTaskFactory { this.connectionReaderFactory = connectionReaderFactory; this.connectionWriterFactory = connectionWriterFactory; this.authorFactory = authorFactory; + this.groupFactory = groupFactory; this.keyManager = keyManager; this.connectionDispatcher = connectionDispatcher; this.clock = clock; @@ -56,7 +59,7 @@ class InvitationTaskFactoryImpl implements InvitationTaskFactory { int remoteCode) { return new ConnectorGroup(crypto, db, readerFactory, writerFactory, connectionReaderFactory, connectionWriterFactory, - authorFactory, keyManager, connectionDispatcher, clock, - pluginManager, localAuthorId, localCode, remoteCode); + authorFactory, groupFactory, keyManager, connectionDispatcher, + clock, pluginManager, localAuthorId, localCode, remoteCode); } } diff --git a/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java index a55a60b220..348672611b 100644 --- a/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java +++ b/briar-core/src/net/sf/briar/messaging/AuthorFactoryImpl.java @@ -27,24 +27,28 @@ class AuthorFactoryImpl implements AuthorFactory { this.writerFactory = writerFactory; } - public Author createAuthor(String name, byte[] publicKey) - throws IOException { + public Author createAuthor(String name, byte[] publicKey) { return new Author(getId(name, publicKey), name, publicKey); } public LocalAuthor createLocalAuthor(String name, byte[] publicKey, - byte[] privateKey) throws IOException { + byte[] privateKey) { return new LocalAuthor(getId(name, publicKey), name, publicKey, privateKey); } - private AuthorId getId(String name, byte[] publicKey) throws IOException { + private AuthorId getId(String name, byte[] publicKey) { ByteArrayOutputStream out = new ByteArrayOutputStream(); Writer w = writerFactory.createWriter(out); - w.writeStructStart(AUTHOR); - w.writeString(name); - w.writeBytes(publicKey); - w.writeStructEnd(); + try { + w.writeStructStart(AUTHOR); + w.writeString(name); + w.writeBytes(publicKey); + w.writeStructEnd(); + } catch(IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(); + } MessageDigest messageDigest = crypto.getMessageDigest(); messageDigest.update(out.toByteArray()); return new AuthorId(messageDigest.digest()); diff --git a/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java index 0a251f523d..bb08c25580 100644 --- a/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java +++ b/briar-core/src/net/sf/briar/messaging/GroupFactoryImpl.java @@ -27,22 +27,28 @@ class GroupFactoryImpl implements GroupFactory { this.writerFactory = writerFactory; } - public Group createGroup(String name) throws IOException { + public Group createGroup(String name, boolean isPrivate) { byte[] salt = new byte[GROUP_SALT_LENGTH]; crypto.getSecureRandom().nextBytes(salt); - return createGroup(name, salt); + return createGroup(name, salt, isPrivate); } - public Group createGroup(String name, byte[] salt) throws IOException { + public Group createGroup(String name, byte[] salt, boolean isPrivate) { ByteArrayOutputStream out = new ByteArrayOutputStream(); Writer w = writerFactory.createWriter(out); - w.writeStructStart(GROUP); - w.writeString(name); - w.writeBytes(salt); - w.writeStructEnd(); + try { + w.writeStructStart(GROUP); + w.writeString(name); + w.writeBytes(salt); + w.writeBoolean(isPrivate); + w.writeStructEnd(); + } catch(IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(); + } MessageDigest messageDigest = crypto.getMessageDigest(); messageDigest.update(out.toByteArray()); GroupId id = new GroupId(messageDigest.digest()); - return new Group(id, name, salt); + return new Group(id, name, salt, isPrivate); } } diff --git a/briar-core/src/net/sf/briar/messaging/GroupReader.java b/briar-core/src/net/sf/briar/messaging/GroupReader.java index 20402c5ae2..4b35b2ec92 100644 --- a/briar-core/src/net/sf/briar/messaging/GroupReader.java +++ b/briar-core/src/net/sf/briar/messaging/GroupReader.java @@ -31,10 +31,11 @@ class GroupReader implements StructReader<Group> { byte[] publicKey = null; if(r.hasNull()) r.readNull(); else publicKey = r.readBytes(MAX_PUBLIC_KEY_LENGTH); + boolean isPrivate = r.readBoolean(); r.readStructEnd(); r.removeConsumer(digesting); // Build and return the group GroupId id = new GroupId(messageDigest.digest()); - return new Group(id, name, publicKey); + return new Group(id, name, publicKey, isPrivate); } } diff --git a/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java index 6f4ab5b11b..cbb60ff59b 100644 --- a/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java +++ b/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java @@ -47,13 +47,6 @@ class MessageFactoryImpl implements MessageFactory { this.writerFactory = writerFactory; } - public Message createPrivateMessage(MessageId parent, String contentType, - long timestamp, byte[] body) throws IOException, - GeneralSecurityException { - return createMessage(parent, null, null, null, contentType, timestamp, - body); - } - public Message createAnonymousMessage(MessageId parent, Group group, String contentType, long timestamp, byte[] body) throws IOException, GeneralSecurityException { @@ -97,8 +90,7 @@ class MessageFactoryImpl implements MessageFactory { w.writeStructStart(MESSAGE); if(parent == null) w.writeNull(); else w.writeBytes(parent.getBytes()); - if(group == null) w.writeNull(); - else writeGroup(w, group); + writeGroup(w, group); if(author == null) w.writeNull(); else writeAuthor(w, author); w.writeString(contentType); @@ -130,6 +122,7 @@ class MessageFactoryImpl implements MessageFactory { w.writeStructStart(GROUP); w.writeString(g.getName()); w.writeBytes(g.getSalt()); + w.writeBoolean(g.isPrivate()); w.writeStructEnd(); } diff --git a/briar-core/src/net/sf/briar/messaging/MessageReader.java b/briar-core/src/net/sf/briar/messaging/MessageReader.java index c3a6978865..365b8c4e1f 100644 --- a/briar-core/src/net/sf/briar/messaging/MessageReader.java +++ b/briar-core/src/net/sf/briar/messaging/MessageReader.java @@ -4,14 +4,10 @@ import static net.sf.briar.api.AuthorConstants.MAX_SIGNATURE_LENGTH; import static net.sf.briar.api.messaging.MessagingConstants.MAX_BODY_LENGTH; import static net.sf.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; import static net.sf.briar.api.messaging.MessagingConstants.MAX_PACKET_LENGTH; -import static net.sf.briar.api.messaging.MessagingConstants.MAX_SUBJECT_LENGTH; import static net.sf.briar.api.messaging.MessagingConstants.MESSAGE_SALT_LENGTH; import static net.sf.briar.api.messaging.Types.MESSAGE; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; import net.sf.briar.api.Author; import net.sf.briar.api.FormatException; @@ -28,13 +24,11 @@ class MessageReader implements StructReader<UnverifiedMessage> { private final StructReader<Group> groupReader; private final StructReader<Author> authorReader; - private final CharsetDecoder decoder; MessageReader(StructReader<Group> groupReader, StructReader<Author> authorReader) { this.groupReader = groupReader; this.authorReader = authorReader; - decoder = Charset.forName("UTF-8").newDecoder(); } public UnverifiedMessage readStruct(Reader r) throws IOException { @@ -53,10 +47,8 @@ class MessageReader implements StructReader<UnverifiedMessage> { if(b.length < UniqueId.LENGTH) throw new FormatException(); parent = new MessageId(b); } - // Read the group, if there is one - Group group = null; - if(r.hasNull()) r.readNull(); - else group = groupReader.readStruct(r); + // Read the group + Group group = groupReader.readStruct(r); // Read the author, if there is one Author author = null; if(r.hasNull()) r.readNull(); @@ -71,16 +63,6 @@ class MessageReader implements StructReader<UnverifiedMessage> { if(salt.length < MESSAGE_SALT_LENGTH) throw new FormatException(); // Read the message body byte[] body = r.readBytes(MAX_BODY_LENGTH); - // If the content type is text/plain, extract a subject line - String subject; - if(contentType.equals("text/plain")) { - byte[] start = new byte[Math.min(MAX_SUBJECT_LENGTH, body.length)]; - System.arraycopy(body, 0, start, 0, start.length); - decoder.reset(); - subject = decoder.decode(ByteBuffer.wrap(start)).toString(); - } else { - subject = ""; - } // Record the offset of the body within the message int bodyStart = (int) counting.getCount() - body.length; // Record the length of the data covered by the author's signature @@ -96,7 +78,7 @@ class MessageReader implements StructReader<UnverifiedMessage> { r.removeConsumer(copying); byte[] raw = copying.getCopy(); return new UnverifiedMessage(parent, group, author, contentType, - subject, timestamp, raw, signature, bodyStart, body.length, + timestamp, raw, signature, bodyStart, body.length, signedLength); } } diff --git a/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java b/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java index ed139c53cc..cf9f755fdb 100644 --- a/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java +++ b/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java @@ -1,10 +1,13 @@ package net.sf.briar.messaging; +import static net.sf.briar.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; + import java.security.GeneralSecurityException; import javax.inject.Inject; import net.sf.briar.api.Author; +import net.sf.briar.api.clock.Clock; import net.sf.briar.api.crypto.CryptoComponent; import net.sf.briar.api.crypto.KeyParser; import net.sf.briar.api.crypto.MessageDigest; @@ -18,11 +21,13 @@ import net.sf.briar.api.messaging.UnverifiedMessage; class MessageVerifierImpl implements MessageVerifier { private final CryptoComponent crypto; + private final Clock clock; private final KeyParser keyParser; @Inject - MessageVerifierImpl(CryptoComponent crypto) { + MessageVerifierImpl(CryptoComponent crypto, Clock clock) { this.crypto = crypto; + this.clock = clock; keyParser = crypto.getSignatureKeyParser(); } @@ -30,7 +35,11 @@ class MessageVerifierImpl implements MessageVerifier { throws GeneralSecurityException { MessageDigest messageDigest = crypto.getMessageDigest(); Signature signature = crypto.getSignature(); - // Hash the message, including the signature, to get the message ID + // Reject the message if it's too far in the future + long now = clock.currentTimeMillis(); + if(m.getTimestamp() > now + MAX_CLOCK_DIFFERENCE) + throw new GeneralSecurityException(); + // Hash the message to get the message ID byte[] raw = m.getSerialised(); messageDigest.update(raw); MessageId id = new MessageId(messageDigest.digest()); diff --git a/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java b/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java index 281d5a36b9..ffdf05b248 100644 --- a/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java +++ b/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java @@ -129,6 +129,7 @@ class PacketWriterImpl implements PacketWriter { w.writeStructStart(GROUP); w.writeString(g.getName()); w.writeBytes(g.getSalt()); + w.writeBoolean(g.isPrivate()); w.writeStructEnd(); } w.writeListEnd(); diff --git a/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java b/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java index 1ed20b5eda..70f628392e 100644 --- a/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java +++ b/briar-core/src/net/sf/briar/messaging/SubscriptionUpdateReader.java @@ -32,10 +32,10 @@ class SubscriptionUpdateReader implements StructReader<SubscriptionUpdate> { // Read the start of the struct r.readStructStart(SUBSCRIPTION_UPDATE); // Read the subscriptions - List<Group> subs = new ArrayList<Group>(); + List<Group> groups = new ArrayList<Group>(); r.readListStart(); for(int i = 0; i < MAX_SUBSCRIPTIONS && !r.hasListEnd(); i++) - subs.add(groupReader.readStruct(r)); + groups.add(groupReader.readStruct(r)); r.readListEnd(); // Read the version number long version = r.readIntAny(); @@ -45,7 +45,7 @@ class SubscriptionUpdateReader implements StructReader<SubscriptionUpdate> { // Reset the reader r.removeConsumer(counting); // Build and return the subscription update - subs = Collections.unmodifiableList(subs); - return new SubscriptionUpdate(subs, version); + groups = Collections.unmodifiableList(groups); + return new SubscriptionUpdate(groups, version); } } diff --git a/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java b/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java index 747a443484..8d70582358 100644 --- a/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java +++ b/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java @@ -25,12 +25,11 @@ import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.GroupMessageAddedEvent; import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent; +import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.MessageReceivedEvent; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent; @@ -129,7 +128,7 @@ abstract class DuplexConnection implements DatabaseListener { if(e instanceof ContactRemovedEvent) { ContactRemovedEvent c = (ContactRemovedEvent) e; if(contactId.equals(c.getContactId())) writerTasks.add(CLOSE); - } else if(e instanceof GroupMessageAddedEvent) { + } else if(e instanceof MessageAddedEvent) { if(canSendOffer.getAndSet(false)) dbExecutor.execute(new GenerateOffer()); } else if(e instanceof MessageExpiredEvent) { @@ -147,12 +146,6 @@ abstract class DuplexConnection implements DatabaseListener { } else if(e instanceof MessageReceivedEvent) { if(((MessageReceivedEvent) e).getContactId().equals(contactId)) dbExecutor.execute(new GenerateAcks()); - } else if(e instanceof PrivateMessageAddedEvent) { - PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e; - if(!p.isIncoming() && p.getContactId().equals(contactId)) { - if(canSendOffer.getAndSet(false)) - dbExecutor.execute(new GenerateOffer()); - } } else if(e instanceof RemoteRetentionTimeUpdatedEvent) { dbExecutor.execute(new GenerateRetentionAck()); } else if(e instanceof RemoteSubscriptionsUpdatedEvent) { diff --git a/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java index 1a4b1c6590..fa108875db 100644 --- a/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java +++ b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java @@ -95,7 +95,7 @@ public class ProtocolIntegrationTest extends BriarTestCase { new Random().nextBytes(secret); // Create a group GroupFactory groupFactory = i.getInstance(GroupFactory.class); - group = groupFactory.createGroup("Group"); + group = groupFactory.createGroup("Group", false); // Create an author AuthorFactory authorFactory = i.getInstance(AuthorFactory.class); CryptoComponent crypto = i.getInstance(CryptoComponent.class); diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java index cc6a37ab37..d33f8b29e6 100644 --- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -31,11 +31,11 @@ import net.sf.briar.api.db.NoSuchTransportException; import net.sf.briar.api.db.event.ContactAddedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.GroupMessageAddedEvent; import net.sf.briar.api.db.event.LocalAuthorAddedEvent; import net.sf.briar.api.db.event.LocalAuthorRemovedEvent; import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; +import net.sf.briar.api.db.event.MessageAddedEvent; +import net.sf.briar.api.db.event.MessageReceivedEvent; import net.sf.briar.api.db.event.SubscriptionAddedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.lifecycle.ShutdownManager; @@ -68,12 +68,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase { protected final Author author; protected final AuthorId localAuthorId; protected final LocalAuthor localAuthor; - protected final MessageId messageId, messageId1, privateMessageId; + protected final MessageId messageId, messageId1; protected final String contentType, subject; protected final long timestamp; protected final int size; protected final byte[] raw; - protected final Message message, privateMessage; + protected final Message message, message1; protected final TransportId transportId; protected final TransportProperties transportProperties; protected final ContactId contactId; @@ -83,7 +83,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { public DatabaseComponentTest() { groupId = new GroupId(TestUtils.getRandomId()); - group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH]); + group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH], true); authorId = new AuthorId(TestUtils.getRandomId()); author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); localAuthorId = new AuthorId(TestUtils.getRandomId()); @@ -91,7 +91,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase { new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100]); messageId = new MessageId(TestUtils.getRandomId()); messageId1 = new MessageId(TestUtils.getRandomId()); - privateMessageId = new MessageId(TestUtils.getRandomId()); contentType = "text/plain"; subject = "Foo"; timestamp = System.currentTimeMillis(); @@ -99,7 +98,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { raw = new byte[size]; message = new TestMessage(messageId, null, group, author, contentType, subject, timestamp, raw); - privateMessage = new TestMessage(privateMessageId, null, null, null, + message1 = new TestMessage(messageId1, messageId, group, null, contentType, subject, timestamp, raw); transportId = new TransportId(TestUtils.getRandomId()); transportProperties = new TransportProperties(Collections.singletonMap( @@ -136,13 +135,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase { with(any(long.class))); oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); will(returnValue(shutdownHandle)); - // addLocalAuthor(localAuthor) + // addLocalAuthor() oneOf(database).containsLocalAuthor(txn, localAuthorId); will(returnValue(false)); oneOf(database).addLocalAuthor(txn, localAuthor); oneOf(listener).eventOccurred(with(any( LocalAuthorAddedEvent.class))); - // addContact(author, localAuthorId) + // addContact() oneOf(database).containsContact(txn, authorId); will(returnValue(false)); oneOf(database).containsLocalAuthor(txn, localAuthorId); @@ -153,43 +152,43 @@ public abstract class DatabaseComponentTest extends BriarTestCase { // getContacts() oneOf(database).getContacts(txn); will(returnValue(Arrays.asList(contact))); - // getRemoteProperties(transportId) + // getRemoteProperties() oneOf(database).getRemoteProperties(txn, transportId); will(returnValue(Collections.emptyMap())); - // subscribe(group) - oneOf(database).containsSubscription(txn, groupId); + // addGroup() + oneOf(database).containsGroup(txn, groupId); will(returnValue(false)); - oneOf(database).addSubscription(txn, group); + oneOf(database).addGroup(txn, group); will(returnValue(true)); oneOf(listener).eventOccurred(with(any( SubscriptionAddedEvent.class))); - // subscribe(group) again - oneOf(database).containsSubscription(txn, groupId); + // addGroup() again + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); - // getMessageHeaders(groupId) - oneOf(database).containsSubscription(txn, groupId); + // getMessageHeaders() + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); - oneOf(database).getGroupMessageHeaders(txn, groupId); + oneOf(database).getMessageHeaders(txn, groupId); will(returnValue(Collections.emptyList())); - // getSubscriptions() - oneOf(database).getSubscriptions(txn); + // getGroups() + oneOf(database).getGroups(txn); will(returnValue(Arrays.asList(groupId))); - // unsubscribe(groupId) - oneOf(database).containsSubscription(txn, groupId); + // removeGroup() + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.emptyList())); - oneOf(database).removeSubscription(txn, groupId); + oneOf(database).removeGroup(txn, groupId); oneOf(listener).eventOccurred(with(any( SubscriptionRemovedEvent.class))); oneOf(listener).eventOccurred(with(any( LocalSubscriptionsUpdatedEvent.class))); - // removeContact(contactId) + // removeContact() oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).removeContact(txn, contactId); oneOf(listener).eventOccurred(with(any(ContactRemovedEvent.class))); - // removeLocalAuthor(localAuthorId) + // removeLocalAuthor() oneOf(database).containsLocalAuthor(txn, localAuthorId); will(returnValue(true)); oneOf(database).getContacts(txn, localAuthorId); @@ -212,12 +211,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { assertEquals(Arrays.asList(contact), db.getContacts()); assertEquals(Collections.emptyMap(), db.getRemoteProperties(transportId)); - db.subscribe(group); // First time - listeners called - db.subscribe(group); // Second time - not called - assertEquals(Collections.emptyList(), - db.getGroupMessageHeaders(groupId)); - assertEquals(Arrays.asList(groupId), db.getSubscriptions()); - db.unsubscribe(group); + db.addGroup(group); // First time - listeners called + db.addGroup(group); // Second time - not called + assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId)); + assertEquals(Arrays.asList(groupId), db.getGroups()); + db.removeGroup(group); db.removeContact(contactId); db.removeLocalAuthor(localAuthorId); db.removeListener(listener); @@ -227,133 +225,82 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } @Test - public void testGroupMessagesAreNotStoredUnlessSubscribed() - throws Exception { + public void testDuplicateLocalMessagesAreNotStored() throws Exception { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database<Object> database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class); context.checking(new Expectations() {{ - // addLocalGroupMessage(message) oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); - will(returnValue(false)); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); - db.addLocalGroupMessage(message); + db.addLocalMessage(message); context.assertIsSatisfied(); } @Test - public void testDuplicateGroupMessagesAreNotStored() throws Exception { + public void testLocalMessagesAreNotStoredUnlessSubscribed() + throws Exception { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database<Object> database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class); context.checking(new Expectations() {{ - // addLocalGroupMessage(message) oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); - will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message, false); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(false)); + oneOf(database).containsGroup(txn, groupId); will(returnValue(false)); oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); - db.addLocalGroupMessage(message); + db.addLocalMessage(message); context.assertIsSatisfied(); } @Test - public void testAddLocalGroupMessage() throws Exception { + public void testAddLocalMessage() throws Exception { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database<Object> database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); context.checking(new Expectations() {{ - // addLocalGroupMessage(message) oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); - will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message, false); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(false)); + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); + oneOf(database).addMessage(txn, message, false); oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).getContactIds(txn); will(returnValue(Arrays.asList(contactId))); oneOf(database).addStatus(txn, contactId, messageId, false); oneOf(database).commitTransaction(txn); + // The message was added, so the listener should be called + oneOf(listener).eventOccurred(with(any( + MessageAddedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); - db.addLocalGroupMessage(message); - - context.assertIsSatisfied(); - } - - @Test - public void testDuplicatePrivateMessagesAreNotStored() throws Exception { - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - context.checking(new Expectations() {{ - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).commitTransaction(txn); - oneOf(database).containsContact(txn, contactId); - will(returnValue(true)); - // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId, - false); - will(returnValue(false)); - }}); - DatabaseComponent db = createDatabaseComponent(database, cleaner, - shutdown); - - db.addLocalPrivateMessage(privateMessage, contactId); - - context.assertIsSatisfied(); - } - - @Test - public void testAddLocalPrivateMessage() throws Exception { - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - context.checking(new Expectations() {{ - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).commitTransaction(txn); - oneOf(database).containsContact(txn, contactId); - will(returnValue(true)); - // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId, - false); - will(returnValue(true)); - oneOf(database).setReadFlag(txn, privateMessageId, true); - oneOf(database).addStatus(txn, contactId, privateMessageId, false); - }}); - DatabaseComponent db = createDatabaseComponent(database, cleaner, - shutdown); - - db.addLocalPrivateMessage(privateMessage, contactId); + db.addListener(listener); + db.addLocalMessage(message); context.assertIsSatisfied(); } @@ -383,7 +330,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } catch(NoSuchContactException expected) {} try { - db.addLocalPrivateMessage(privateMessage, contactId); + db.containsSendableMessages(contactId); fail(); } catch(NoSuchContactException expected) {} @@ -443,12 +390,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } catch(NoSuchContactException expected) {} try { - db.getVisibleSubscriptions(contactId); - fail(); - } catch(NoSuchContactException expected) {} - - try { - db.hasSendableMessages(contactId); + db.getInboxGroup(contactId); fail(); } catch(NoSuchContactException expected) {} @@ -522,6 +464,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { fail(); } catch(NoSuchContactException expected) {} + try { + db.setInboxGroup(contactId, group); + fail(); + } catch(NoSuchContactException expected) {} + try { db.setSeen(contactId, Arrays.asList(messageId)); fail(); @@ -582,7 +529,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { // Check whether the subscription is in the DB (which it's not) exactly(5).of(database).startTransaction(); will(returnValue(txn)); - exactly(5).of(database).containsSubscription(txn, groupId); + exactly(5).of(database).containsGroup(txn, groupId); will(returnValue(false)); exactly(5).of(database).abortTransaction(txn); }}); @@ -595,7 +542,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } catch(NoSuchSubscriptionException expected) {} try { - db.getGroupMessageHeaders(groupId); + db.getMessageHeaders(groupId); fail(); } catch(NoSuchSubscriptionException expected) {} @@ -605,12 +552,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } catch(NoSuchSubscriptionException expected) {} try { - db.setVisibility(groupId, Collections.<ContactId>emptyList()); + db.removeGroup(group); fail(); } catch(NoSuchSubscriptionException expected) {} try { - db.unsubscribe(group); + db.setVisibility(groupId, Collections.<ContactId>emptyList()); fail(); } catch(NoSuchSubscriptionException expected) {} @@ -626,14 +573,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase { final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class); context.checking(new Expectations() {{ - // addLocalAuthor(localAuthor) + // addLocalAuthor() oneOf(database).startTransaction(); will(returnValue(txn)); oneOf(database).containsLocalAuthor(txn, localAuthorId); will(returnValue(false)); oneOf(database).addLocalAuthor(txn, localAuthor); oneOf(database).commitTransaction(txn); - // addContact(author, localAuthorId) + // addContact() oneOf(database).startTransaction(); will(returnValue(txn)); oneOf(database).containsContact(txn, authorId); @@ -830,12 +777,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // Get the sendable message IDs oneOf(database).getMessagesToOffer(txn, contactId, 123); will(returnValue(messagesToOffer)); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -856,11 +803,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).getRetentionUpdate(txn, contactId, Long.MAX_VALUE); will(returnValue(null)); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -880,11 +827,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).getRetentionUpdate(txn, contactId, Long.MAX_VALUE); will(returnValue(new RetentionUpdate(0, 1))); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -907,12 +854,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).getSubscriptionUpdate(txn, contactId, Long.MAX_VALUE); will(returnValue(null)); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -932,12 +879,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).getSubscriptionUpdate(txn, contactId, Long.MAX_VALUE); will(returnValue(new SubscriptionUpdate(Arrays.asList(group), 1))); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -960,11 +907,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).getTransportUpdates(txn, contactId, Long.MAX_VALUE); will(returnValue(null)); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -984,12 +931,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).getTransportUpdates(txn, contactId, Long.MAX_VALUE); will(returnValue(Arrays.asList(new TransportUpdate(transportId, transportProperties, 1)))); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1016,12 +963,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - // Get the acked messages - oneOf(database).removeOutstandingMessages(txn, contactId, - Arrays.asList(messageId)); + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1032,65 +977,77 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } @Test - public void testReceivePrivateMessage() throws Exception { + public void testReceiveMessage() throws Exception { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database<Object> database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - // The message is stored - oneOf(database).addPrivateMessage(txn, privateMessage, contactId, - true); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(false)); + oneOf(database).containsVisibleGroup(txn, contactId, groupId); will(returnValue(true)); - oneOf(database).addStatus(txn, contactId, privateMessageId, true); - // The message must be acked - oneOf(database).addMessageToAck(txn, contactId, privateMessageId); + oneOf(database).addMessage(txn, message, true); + oneOf(database).addStatus(txn, contactId, messageId, true); + oneOf(database).getContactIds(txn); + will(returnValue(Arrays.asList(contactId))); + oneOf(database).addMessageToAck(txn, contactId, messageId); + oneOf(database).commitTransaction(txn); + // The message was received and added + oneOf(listener).eventOccurred(with(any( + MessageReceivedEvent.class))); + oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); - db.receiveMessage(contactId, privateMessage); + db.addListener(listener); + db.receiveMessage(contactId, message); context.assertIsSatisfied(); } @Test - public void testReceiveDuplicatePrivateMessage() throws Exception { + public void testReceiveDuplicateMessage() throws Exception { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database<Object> database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - // The message is not stored, it's a duplicate - oneOf(database).addPrivateMessage(txn, privateMessage, contactId, - true); - will(returnValue(false)); - // The message must still be acked - oneOf(database).addMessageToAck(txn, contactId, privateMessageId); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).containsVisibleGroup(txn, contactId, groupId); + will(returnValue(true)); + // The message wasn't stored but it must still be acked + oneOf(database).addMessageToAck(txn, contactId, messageId); + oneOf(database).commitTransaction(txn); + // The message was received but not added + oneOf(listener).eventOccurred(with(any( + MessageReceivedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); - db.receiveMessage(contactId, privateMessage); + db.addListener(listener); + db.receiveMessage(contactId, message); context.assertIsSatisfied(); } @Test - public void testReceiveMessageDoesNotStoreGroupMessageUnlessSubscribed() - throws Exception { + public void testReceiveMessageWithoutVisibleGroup() throws Exception { Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database<Object> database = context.mock(Database.class); @@ -1099,15 +1056,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - // Only store messages belonging to visible, subscribed groups - oneOf(database).containsVisibleSubscription(txn, contactId, - groupId); + oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - // The message is not stored but it must still be acked - oneOf(database).addMessageToAck(txn, contactId, messageId); + oneOf(database).containsVisibleGroup(txn, contactId, groupId); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1132,7 +1087,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // Get the offered messages @@ -1142,6 +1096,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(true)); // Visible - ack message # 1 oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId2); will(returnValue(false)); // Not visible - request message # 2 + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1168,10 +1123,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).setRetentionUpdateAcked(txn, contactId, 1); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1192,10 +1147,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).setSubscriptionUpdateAcked(txn, contactId, 1); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1216,11 +1171,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - oneOf(database).setSubscriptions(txn, contactId, - Arrays.asList(group), 1); + oneOf(database).setGroups(txn, contactId, Arrays.asList(group), 1); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1241,13 +1195,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).containsTransport(txn, transportId); will(returnValue(true)); oneOf(database).setTransportUpdateAcked(txn, contactId, transportId, 1); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1268,11 +1222,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); oneOf(database).setRemoteProperties(txn, contactId, transportId, transportProperties, 1); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1284,132 +1238,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.assertIsSatisfied(); } - @Test - public void testAddingGroupMessageCallsListeners() throws Exception { - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - final DatabaseListener listener = context.mock(DatabaseListener.class); - context.checking(new Expectations() {{ - // addLocalGroupMessage(message) - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); - will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message, false); - will(returnValue(true)); - oneOf(database).setReadFlag(txn, messageId, true); - oneOf(database).getContactIds(txn); - will(returnValue(Arrays.asList(contactId))); - oneOf(database).addStatus(txn, contactId, messageId, false); - oneOf(database).commitTransaction(txn); - // The message was added, so the listener should be called - oneOf(listener).eventOccurred(with(any( - GroupMessageAddedEvent.class))); - }}); - DatabaseComponent db = createDatabaseComponent(database, cleaner, - shutdown); - - db.addListener(listener); - db.addLocalGroupMessage(message); - - context.assertIsSatisfied(); - } - - @Test - public void testAddingPrivateMessageCallsListeners() throws Exception { - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - final DatabaseListener listener = context.mock(DatabaseListener.class); - context.checking(new Expectations() {{ - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).commitTransaction(txn); - oneOf(database).containsContact(txn, contactId); - will(returnValue(true)); - // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId, - false); - will(returnValue(true)); - oneOf(database).setReadFlag(txn, privateMessageId, true); - oneOf(database).addStatus(txn, contactId, privateMessageId, false); - // The message was added, so the listener should be called - oneOf(listener).eventOccurred(with(any( - PrivateMessageAddedEvent.class))); - }}); - DatabaseComponent db = createDatabaseComponent(database, cleaner, - shutdown); - - db.addListener(listener); - db.addLocalPrivateMessage(privateMessage, contactId); - - context.assertIsSatisfied(); - } - - @Test - public void testAddingDuplicateGroupMessageDoesNotCallListeners() - throws Exception { - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - final DatabaseListener listener = context.mock(DatabaseListener.class); - context.checking(new Expectations() {{ - // addLocalGroupMessage(message) - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); - will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message, false); - will(returnValue(false)); - oneOf(database).commitTransaction(txn); - // The message was not added, so the listener should not be called - }}); - DatabaseComponent db = createDatabaseComponent(database, cleaner, - shutdown); - - db.addListener(listener); - db.addLocalGroupMessage(message); - - context.assertIsSatisfied(); - } - - @Test - public void testAddingDuplicatePrivateMessageDoesNotCallListeners() - throws Exception { - Mockery context = new Mockery(); - @SuppressWarnings("unchecked") - final Database<Object> database = context.mock(Database.class); - final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final ShutdownManager shutdown = context.mock(ShutdownManager.class); - final DatabaseListener listener = context.mock(DatabaseListener.class); - context.checking(new Expectations() {{ - oneOf(database).startTransaction(); - will(returnValue(txn)); - oneOf(database).commitTransaction(txn); - oneOf(database).containsContact(txn, contactId); - will(returnValue(true)); - // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId, - false); - will(returnValue(false)); - // The message was not added, so the listener should not be called - }}); - DatabaseComponent db = createDatabaseComponent(database, cleaner, - shutdown); - - db.addListener(listener); - db.addLocalPrivateMessage(privateMessage, contactId); - - context.assertIsSatisfied(); - } - @Test public void testChangingLocalTransportPropertiesCallsListeners() throws Exception { @@ -1477,11 +1305,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - // setSeen(contactId, Arrays.asList(messageId)) oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId); + oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -1504,7 +1331,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getVisibility(txn, groupId); will(returnValue(both)); @@ -1539,7 +1366,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getVisibility(txn, groupId); will(returnValue(both)); @@ -1558,7 +1385,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } @Test - public void testSettingVisibleToAllTrueAffectsCurrentContacts() + public void testSettingVisibleToAllAffectsCurrentContacts() throws Exception { final ContactId contactId1 = new ContactId(123); final Collection<ContactId> both = Arrays.asList(contactId, contactId1); @@ -1572,7 +1399,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { // setVisibility() oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.emptyList())); @@ -1586,7 +1413,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { // setVisibleToAll() oneOf(database).startTransaction(); will(returnValue(txn)); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).setVisibleToAll(txn, groupId, true); oneOf(database).getVisibility(txn, groupId); diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java index 82e9ef580b..1d29d1a7fe 100644 --- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java +++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java @@ -33,7 +33,7 @@ import net.sf.briar.api.TransportId; import net.sf.briar.api.TransportProperties; import net.sf.briar.api.clock.SystemClock; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.GroupMessageHeader; +import net.sf.briar.api.db.MessageHeader; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.GroupStatus; @@ -59,25 +59,24 @@ public class H2DatabaseTest extends BriarTestCase { private final Author author; private final AuthorId localAuthorId; private final LocalAuthor localAuthor; - private final MessageId messageId, messageId1; + private final MessageId messageId; private final String contentType, subject; private final long timestamp; private final int size; private final byte[] raw; - private final Message message, privateMessage; + private final Message message; private final TransportId transportId; private final ContactId contactId; public H2DatabaseTest() throws Exception { groupId = new GroupId(TestUtils.getRandomId()); - group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH]); + group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH], false); authorId = new AuthorId(TestUtils.getRandomId()); author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); localAuthorId = new AuthorId(TestUtils.getRandomId()); localAuthor = new LocalAuthor(localAuthorId, "Bob", new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100]); messageId = new MessageId(TestUtils.getRandomId()); - messageId1 = new MessageId(TestUtils.getRandomId()); contentType = "text/plain"; subject = "Foo"; timestamp = System.currentTimeMillis(); @@ -86,8 +85,6 @@ public class H2DatabaseTest extends BriarTestCase { random.nextBytes(raw); message = new TestMessage(messageId, null, group, author, contentType, subject, timestamp, raw); - privateMessage = new TestMessage(messageId1, null, null, null, - contentType, subject, timestamp, raw); transportId = new TransportId(TestUtils.getRandomId()); contactId = new ContactId(1); } @@ -106,15 +103,12 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); assertTrue(db.containsContact(txn, contactId)); - assertFalse(db.containsSubscription(txn, groupId)); - db.addSubscription(txn, group); - assertTrue(db.containsSubscription(txn, groupId)); + assertFalse(db.containsGroup(txn, groupId)); + db.addGroup(txn, group); + assertTrue(db.containsGroup(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - db.addGroupMessage(txn, message, true); + db.addMessage(txn, message, true); assertTrue(db.containsMessage(txn, messageId)); - assertFalse(db.containsMessage(txn, messageId1)); - db.addPrivateMessage(txn, privateMessage, contactId, true); - assertTrue(db.containsMessage(txn, messageId1)); db.commitTransaction(txn); db.close(); @@ -122,18 +116,14 @@ public class H2DatabaseTest extends BriarTestCase { db = open(true); txn = db.startTransaction(); assertTrue(db.containsContact(txn, contactId)); - assertTrue(db.containsSubscription(txn, groupId)); + assertTrue(db.containsGroup(txn, groupId)); assertTrue(db.containsMessage(txn, messageId)); byte[] raw1 = db.getRawMessage(txn, messageId); assertArrayEquals(raw, raw1); - assertTrue(db.containsMessage(txn, messageId1)); - raw1 = db.getRawMessage(txn, messageId1); - assertArrayEquals(raw, raw1); // Delete the records db.removeMessage(txn, messageId); - db.removeMessage(txn, messageId1); db.removeContact(txn, contactId); - db.removeSubscription(txn, groupId); + db.removeGroup(txn, groupId); db.commitTransaction(txn); db.close(); @@ -143,25 +133,24 @@ public class H2DatabaseTest extends BriarTestCase { assertFalse(db.containsContact(txn, contactId)); assertEquals(Collections.emptyMap(), db.getRemoteProperties(txn, transportId)); - assertFalse(db.containsSubscription(txn, groupId)); + assertFalse(db.containsGroup(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - assertFalse(db.containsMessage(txn, messageId1)); db.commitTransaction(txn); db.close(); } @Test - public void testUnsubscribingRemovesGroupMessage() throws Exception { + public void testUnsubscribingRemovesMessage() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Subscribe to a group and store a message - db.addSubscription(txn, group); - db.addGroupMessage(txn, message, false); + db.addGroup(txn, group); + db.addMessage(txn, message, false); // Unsubscribing from the group should remove the message assertTrue(db.containsMessage(txn, messageId)); - db.removeSubscription(txn, groupId); + db.removeGroup(txn, groupId); assertFalse(db.containsMessage(txn, messageId)); db.commitTransaction(txn); @@ -169,105 +158,27 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testRemovingContactRemovesPrivateMessage() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and store a private message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addPrivateMessage(txn, privateMessage, contactId, false); - - // Removing the contact should remove the message - assertTrue(db.containsMessage(txn, messageId1)); - db.removeContact(txn, contactId); - assertFalse(db.containsMessage(txn, messageId1)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendablePrivateMessagesMustHaveSeenFlagFalse() - throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and store a private message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addPrivateMessage(txn, privateMessage, contactId, false); - - // The message has no status yet, so it should not be sendable - assertFalse(db.hasSendableMessages(txn, contactId)); - Iterator<MessageId> it = - db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); - assertFalse(it.hasNext()); - - // Adding a status with seen = false should make the message sendable - db.addStatus(txn, contactId, messageId1, false); - assertTrue(db.hasSendableMessages(txn, contactId)); - it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); - assertTrue(it.hasNext()); - assertEquals(messageId1, it.next()); - assertFalse(it.hasNext()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendablePrivateMessagesMustFitCapacity() - throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and store a private message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addPrivateMessage(txn, privateMessage, contactId, false); - db.addStatus(txn, contactId, messageId1, false); - - // The message is sendable, but too large to send - assertTrue(db.hasSendableMessages(txn, contactId)); - Iterator<MessageId> it = - db.getSendableMessages(txn, contactId, size - 1).iterator(); - assertFalse(it.hasNext()); - - // The message is just the right size to send - assertTrue(db.hasSendableMessages(txn, contactId)); - it = db.getSendableMessages(txn, contactId, size).iterator(); - assertTrue(it.hasNext()); - assertEquals(messageId1, it.next()); - assertFalse(it.hasNext()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendableGroupMessagesMustHaveSeenFlagFalse() - throws Exception { + public void testSendableMessagesMustHaveSeenFlagFalse() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); // The message has no status yet, so it should not be sendable - assertFalse(db.hasSendableMessages(txn, contactId)); + assertFalse(db.containsSendableMessages(txn, contactId)); Iterator<MessageId> it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // Adding a status with seen = false should make the message sendable db.addStatus(txn, contactId, messageId, false); - assertTrue(db.hasSendableMessages(txn, contactId)); + assertTrue(db.containsSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -275,7 +186,7 @@ public class H2DatabaseTest extends BriarTestCase { // Changing the status to seen = true should make the message unsendable db.setStatusSeenIfVisible(txn, contactId, messageId); - assertFalse(db.hasSendableMessages(txn, contactId)); + assertFalse(db.containsSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); @@ -284,35 +195,35 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testSendableGroupMessagesMustBeSubscribed() throws Exception { + public void testSendableMessagesMustBeSubscribed() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addGroupMessage(txn, message, false); + db.addMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // The contact is not subscribed, so the message should not be sendable - assertFalse(db.hasSendableMessages(txn, contactId)); + assertFalse(db.containsSendableMessages(txn, contactId)); Iterator<MessageId> it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // The contact subscribing should make the message sendable - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - assertTrue(db.hasSendableMessages(txn, contactId)); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + assertTrue(db.containsSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); assertFalse(it.hasNext()); // The contact unsubscribing should make the message unsendable - db.setSubscriptions(txn, contactId, Collections.<Group>emptyList(), 2); - assertFalse(db.hasSendableMessages(txn, contactId)); + db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2); + assertFalse(db.containsSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); @@ -321,27 +232,27 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testSendableGroupMessagesMustFitCapacity() throws Exception { + public void testSendableMessagesMustFitCapacity() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // The message is sendable, but too large to send - assertTrue(db.hasSendableMessages(txn, contactId)); + assertTrue(db.containsSendableMessages(txn, contactId)); Iterator<MessageId> it = db.getSendableMessages(txn, contactId, size - 1).iterator(); assertFalse(it.hasNext()); // The message is just the right size to send - assertTrue(db.hasSendableMessages(txn, contactId)); + assertTrue(db.containsSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, size).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -352,28 +263,28 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testSendableGroupMessagesMustBeVisible() throws Exception { + public void testSendableMessagesMustBeVisible() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.addGroup(txn, group); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // The subscription is not visible to the contact, so the message // should not be sendable - assertFalse(db.hasSendableMessages(txn, contactId)); + assertFalse(db.containsSendableMessages(txn, contactId)); Iterator<MessageId> it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // Making the subscription visible should make the message sendable db.addVisibility(txn, contactId, groupId); - assertTrue(db.hasSendableMessages(txn, contactId)); + assertTrue(db.containsSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -389,6 +300,7 @@ public class H2DatabaseTest extends BriarTestCase { Connection txn = db.startTransaction(); // Add a contact and some messages to ack + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addMessageToAck(txn, contactId, messageId); @@ -443,10 +355,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // Retrieve the message from the database and mark it as sent @@ -463,7 +375,7 @@ public class H2DatabaseTest extends BriarTestCase { assertFalse(it.hasNext()); // Pretend that the message was acked - db.removeOutstandingMessages(txn, contactId, Arrays.asList(messageId)); + db.setStatusSeenIfVisible(txn, contactId, messageId); // The message still should not be sendable it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); @@ -482,9 +394,9 @@ public class H2DatabaseTest extends BriarTestCase { Connection txn = db.startTransaction(); // Subscribe to a group and store two messages - db.addSubscription(txn, group); - db.addGroupMessage(txn, message, false); - db.addGroupMessage(txn, message1, false); + db.addGroup(txn, group); + db.addMessage(txn, message, false); + db.addMessage(txn, message1, false); // Allowing enough capacity for one message should return the older one Iterator<MessageId> it = db.getOldMessages(txn, size).iterator(); @@ -507,7 +419,7 @@ public class H2DatabaseTest extends BriarTestCase { public void testGetFreeSpace() throws Exception { byte[] largeBody = new byte[ONE_MEGABYTE]; for(int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i; - Message message1 = new TestMessage(messageId, null, group, author, + Message message = new TestMessage(messageId, null, group, author, contentType, subject, timestamp, largeBody); Database<Connection> db = open(false); @@ -521,8 +433,8 @@ public class H2DatabaseTest extends BriarTestCase { // Storing a message should reduce the free space Connection txn = db.startTransaction(); - db.addSubscription(txn, group); - db.addGroupMessage(txn, message1, false); + db.addGroup(txn, group); + db.addMessage(txn, message, false); db.commitTransaction(txn); assertTrue(db.getFreeSpace() < free); @@ -741,9 +653,9 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and subscribe to a group db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); + db.setGroups(txn, contactId, Arrays.asList(group), 1); // The message is not in the database assertNull(db.getRawMessageIfSendable(txn, contactId, messageId)); @@ -760,10 +672,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); // Set the status to seen = true db.addStatus(txn, contactId, messageId, true); @@ -784,11 +696,11 @@ public class H2DatabaseTest extends BriarTestCase { // the message is older than the contact's retention time db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); + db.setGroups(txn, contactId, Arrays.asList(group), 1); db.setRetentionTime(txn, contactId, timestamp + 1, 1); - db.addGroupMessage(txn, message, false); + db.addMessage(txn, message, false); // Set the status to seen = false db.addStatus(txn, contactId, messageId, false); @@ -808,10 +720,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); // Set the status to seen = false db.addStatus(txn, contactId, messageId, false); @@ -833,9 +745,9 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and subscribe to a group db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); + db.setGroups(txn, contactId, Arrays.asList(group), 1); // The message is not in the database assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); @@ -853,7 +765,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact with a subscription db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); + db.setGroups(txn, contactId, Arrays.asList(group), 1); // There's no local subscription for the group assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); @@ -871,9 +783,9 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addGroupMessage(txn, message, false); + db.addMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // There's no contact subscription for the group @@ -892,9 +804,9 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.addGroup(txn, group); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // The subscription is not visible @@ -913,10 +825,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); // The message has already been seen by the contact db.addStatus(txn, contactId, messageId, true); @@ -936,10 +848,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact, subscribe to a group and store a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message, false); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.addMessage(txn, message, false); // The message has not been seen by the contact db.addStatus(txn, contactId, messageId, false); @@ -958,7 +870,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and subscribe to a group db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); // The group should not be visible to the contact assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); @@ -976,59 +888,59 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testGetGroupMessageParentWithNoParent() throws Exception { + public void testGetParentWithNoParent() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Subscribe to a group - db.addSubscription(txn, group); + db.addGroup(txn, group); // A message with no parent should return null MessageId childId = new MessageId(TestUtils.getRandomId()); Message child = new TestMessage(childId, null, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child, false); + db.addMessage(txn, child, false); assertTrue(db.containsMessage(txn, childId)); - assertNull(db.getGroupMessageParent(txn, childId)); + assertNull(db.getParent(txn, childId)); db.commitTransaction(txn); db.close(); } @Test - public void testGetGroupMessageParentWithAbsentParent() throws Exception { + public void testGetParentWithAbsentParent() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Subscribe to a group - db.addSubscription(txn, group); + db.addGroup(txn, group); // A message with an absent parent should return null MessageId childId = new MessageId(TestUtils.getRandomId()); MessageId parentId = new MessageId(TestUtils.getRandomId()); Message child = new TestMessage(childId, parentId, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child, false); + db.addMessage(txn, child, false); assertTrue(db.containsMessage(txn, childId)); assertFalse(db.containsMessage(txn, parentId)); - assertNull(db.getGroupMessageParent(txn, childId)); + assertNull(db.getParent(txn, childId)); db.commitTransaction(txn); db.close(); } @Test - public void testGetGroupMessageParentWithParentInAnotherGroup() + public void testGetParentWithParentInAnotherGroup() throws Exception { GroupId groupId1 = new GroupId(TestUtils.getRandomId()); Group group1 = new Group(groupId1, "Another group", - new byte[GROUP_SALT_LENGTH]); + new byte[GROUP_SALT_LENGTH], false); Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Subscribe to two groups - db.addSubscription(txn, group); - db.addSubscription(txn, group1); + db.addGroup(txn, group); + db.addGroup(txn, group1); // A message with a parent in another group should return null MessageId childId = new MessageId(TestUtils.getRandomId()); @@ -1037,48 +949,24 @@ public class H2DatabaseTest extends BriarTestCase { contentType, subject, timestamp, raw); Message parent = new TestMessage(parentId, null, group1, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child, false); - db.addGroupMessage(txn, parent, false); + db.addMessage(txn, child, false); + db.addMessage(txn, parent, false); assertTrue(db.containsMessage(txn, childId)); assertTrue(db.containsMessage(txn, parentId)); - assertNull(db.getGroupMessageParent(txn, childId)); + assertNull(db.getParent(txn, childId)); db.commitTransaction(txn); db.close(); } @Test - public void testGetGroupMessageParentWithPrivateParent() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and subscribe to a group - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); - - // A message with a private parent should return null - MessageId childId = new MessageId(TestUtils.getRandomId()); - Message child = new TestMessage(childId, messageId1, group, null, - contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child, false); - db.addPrivateMessage(txn, privateMessage, contactId, false); - assertTrue(db.containsMessage(txn, childId)); - assertTrue(db.containsMessage(txn, messageId1)); - assertNull(db.getGroupMessageParent(txn, childId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetGroupMessageParentWithParentInSameGroup() + public void testGetParentWithParentInSameGroup() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); // Subscribe to a group - db.addSubscription(txn, group); + db.addGroup(txn, group); // A message with a parent in the same group should return the parent MessageId childId = new MessageId(TestUtils.getRandomId()); @@ -1087,11 +975,11 @@ public class H2DatabaseTest extends BriarTestCase { contentType, subject, timestamp, raw); Message parent = new TestMessage(parentId, null, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child, false); - db.addGroupMessage(txn, parent, false); + db.addMessage(txn, child, false); + db.addMessage(txn, parent, false); assertTrue(db.containsMessage(txn, childId)); assertTrue(db.containsMessage(txn, parentId)); - assertEquals(parentId, db.getGroupMessageParent(txn, childId)); + assertEquals(parentId, db.getParent(txn, childId)); db.commitTransaction(txn); db.close(); @@ -1105,16 +993,17 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and subscribe to a group db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addSubscription(txn, group); + db.addGroup(txn, group); // Store a couple of messages int bodyLength = raw.length - 20; - Message message1 = new TestMessage(messageId, null, group, null, + Message message = new TestMessage(messageId, null, group, null, contentType, subject, timestamp, raw, 5, bodyLength); - Message privateMessage1 = new TestMessage(messageId1, null, null, - null, contentType, subject, timestamp, raw, 10, bodyLength); - db.addGroupMessage(txn, message1, false); - db.addPrivateMessage(txn, privateMessage1, contactId, false); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, group, null, + contentType, subject, timestamp, raw, 10, bodyLength); + db.addMessage(txn, message, false); + db.addMessage(txn, message1, false); // Calculate the expected message bodies byte[] expectedBody = new byte[bodyLength]; @@ -1144,27 +1033,27 @@ public class H2DatabaseTest extends BriarTestCase { Connection txn = db.startTransaction(); // Subscribe to a group - db.addSubscription(txn, group); + db.addGroup(txn, group); // Store a couple of messages - db.addGroupMessage(txn, message, false); + db.addMessage(txn, message, false); MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId parentId = new MessageId(TestUtils.getRandomId()); long timestamp1 = System.currentTimeMillis(); Message message1 = new TestMessage(messageId1, parentId, group, author, contentType, subject, timestamp1, raw); - db.addGroupMessage(txn, message1, false); + db.addMessage(txn, message1, false); // Mark one of the messages read assertFalse(db.setReadFlag(txn, messageId, true)); // Retrieve the message headers - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(txn, groupId); - Iterator<GroupMessageHeader> it = headers.iterator(); + Collection<MessageHeader> headers = + db.getMessageHeaders(txn, groupId); + Iterator<MessageHeader> it = headers.iterator(); boolean messageFound = false, message1Found = false; // First header (order is undefined) assertTrue(it.hasNext()); - GroupMessageHeader header = it.next(); + MessageHeader header = it.next(); if(messageId.equals(header.getId())) { assertHeadersMatch(message, header); assertTrue(header.isRead()); @@ -1199,7 +1088,7 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } - private void assertHeadersMatch(Message m, GroupMessageHeader h) { + private void assertHeadersMatch(Message m, MessageHeader h) { assertEquals(m.getId(), h.getId()); if(m.getParent() == null) assertNull(h.getParent()); else assertEquals(m.getParent(), h.getParent()); @@ -1216,8 +1105,8 @@ public class H2DatabaseTest extends BriarTestCase { Connection txn = db.startTransaction(); // Subscribe to a group and store a message - db.addSubscription(txn, group); - db.addGroupMessage(txn, message, false); + db.addGroup(txn, group); + db.addMessage(txn, message, false); // The message should be unread by default assertFalse(db.getReadFlag(txn, messageId)); @@ -1230,7 +1119,7 @@ public class H2DatabaseTest extends BriarTestCase { assertTrue(db.setReadFlag(txn, messageId, false)); assertFalse(db.setReadFlag(txn, messageId, false)); // Unsubscribe from the group - db.removeSubscription(txn, groupId); + db.removeGroup(txn, groupId); db.commitTransaction(txn); db.close(); @@ -1242,24 +1131,24 @@ public class H2DatabaseTest extends BriarTestCase { Connection txn = db.startTransaction(); // Subscribe to a couple of groups - db.addSubscription(txn, group); + db.addGroup(txn, group); GroupId groupId1 = new GroupId(TestUtils.getRandomId()); Group group1 = new Group(groupId1, "Another group", - new byte[GROUP_SALT_LENGTH]); - db.addSubscription(txn, group1); + new byte[GROUP_SALT_LENGTH], false); + db.addGroup(txn, group1); // Store two messages in the first group - db.addGroupMessage(txn, message, false); + db.addMessage(txn, message, false); MessageId messageId1 = new MessageId(TestUtils.getRandomId()); Message message1 = new TestMessage(messageId1, null, group, author, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, message1, false); + db.addMessage(txn, message1, false); // Store one message in the second group MessageId messageId2 = new MessageId(TestUtils.getRandomId()); Message message2 = new TestMessage(messageId2, null, group1, author, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, message2, false); + db.addMessage(txn, message2, false); // Mark one of the messages in the first group read assertFalse(db.setReadFlag(txn, messageId, true)); @@ -1299,7 +1188,7 @@ public class H2DatabaseTest extends BriarTestCase { for(int i = 0; i < 100; i++) { GroupId id = new GroupId(TestUtils.getRandomId()); String name = "Group " + i; - groups.add(new Group(id, name, new byte[GROUP_SALT_LENGTH])); + groups.add(new Group(id, name, new byte[GROUP_SALT_LENGTH], false)); } Database<Connection> db = open(false); @@ -1308,7 +1197,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and subscribe to the groups db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - for(Group g : groups) db.addSubscription(txn, g); + for(Group g : groups) db.addGroup(txn, g); // Make the groups visible to the contact Collections.shuffle(groups); @@ -1319,7 +1208,7 @@ public class H2DatabaseTest extends BriarTestCase { for(Group g : groups) { if(Math.random() < 0.5) db.removeVisibility(txn, contactId, g.getId()); - db.removeSubscription(txn, g.getId()); + db.removeGroup(txn, g.getId()); } db.commitTransaction(txn); @@ -1644,11 +1533,11 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); assertEquals(contactId1, db.addContact(txn, author1, localAuthorId)); - db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.setSubscriptions(txn, contactId1, Arrays.asList(group), 1); + db.setGroups(txn, contactId, Arrays.asList(group), 1); + db.setGroups(txn, contactId1, Arrays.asList(group), 1); // The group should be available, not subscribed, not visible to all - assertEquals(Collections.emptyList(), db.getSubscriptions(txn)); + assertEquals(Collections.emptyList(), db.getGroups(txn)); Iterator<GroupStatus> it = db.getAvailableGroups(txn).iterator(); assertTrue(it.hasNext()); GroupStatus status = it.next(); @@ -1659,8 +1548,8 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to the group - it should be available, subscribed, // not visible to all - db.addSubscription(txn, group); - assertEquals(Arrays.asList(group), db.getSubscriptions(txn)); + db.addGroup(txn, group); + assertEquals(Arrays.asList(group), db.getGroups(txn)); it = db.getAvailableGroups(txn).iterator(); assertTrue(it.hasNext()); status = it.next(); @@ -1671,8 +1560,8 @@ public class H2DatabaseTest extends BriarTestCase { // The first contact unsubscribes - the group should be available, // subscribed, not visible to all - db.setSubscriptions(txn, contactId, Collections.<Group>emptyList(), 2); - assertEquals(Arrays.asList(group), db.getSubscriptions(txn)); + db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2); + assertEquals(Arrays.asList(group), db.getGroups(txn)); it = db.getAvailableGroups(txn).iterator(); assertTrue(it.hasNext()); status = it.next(); @@ -1684,7 +1573,7 @@ public class H2DatabaseTest extends BriarTestCase { // Make the group visible to all contacts - it should be available, // subscribed, visible to all db.setVisibleToAll(txn, groupId, true); - assertEquals(Arrays.asList(group), db.getSubscriptions(txn)); + assertEquals(Arrays.asList(group), db.getGroups(txn)); it = db.getAvailableGroups(txn).iterator(); assertTrue(it.hasNext()); status = it.next(); @@ -1695,8 +1584,8 @@ public class H2DatabaseTest extends BriarTestCase { // Unsubscribe from the group - it should be available, not subscribed, // not visible to all - db.removeSubscription(txn, groupId); - assertEquals(Collections.emptyList(), db.getSubscriptions(txn)); + db.removeGroup(txn, groupId); + assertEquals(Collections.emptyList(), db.getGroups(txn)); it = db.getAvailableGroups(txn).iterator(); assertTrue(it.hasNext()); status = it.next(); @@ -1707,8 +1596,8 @@ public class H2DatabaseTest extends BriarTestCase { // The second contact unsubscribes - the group should no longer be // available - db.setSubscriptions(txn, contactId1, Collections.<Group>emptyList(), 2); - assertEquals(Collections.emptyList(), db.getSubscriptions(txn)); + db.setGroups(txn, contactId1, Collections.<Group>emptyList(), 2); + assertEquals(Collections.emptyList(), db.getGroups(txn)); assertEquals(Collections.emptyList(), db.getAvailableGroups(txn)); db.commitTransaction(txn); diff --git a/briar-tests/src/net/sf/briar/messaging/ConstantsTest.java b/briar-tests/src/net/sf/briar/messaging/ConstantsTest.java index 5534c73d95..b5065a898f 100644 --- a/briar-tests/src/net/sf/briar/messaging/ConstantsTest.java +++ b/briar-tests/src/net/sf/briar/messaging/ConstantsTest.java @@ -121,7 +121,7 @@ public class ConstantsTest extends BriarTestCase { MessageId parent = new MessageId(TestUtils.getRandomId()); // Create a maximum-length group String groupName = TestUtils.createRandomString(MAX_GROUP_NAME_LENGTH); - Group group = groupFactory.createGroup(groupName); + Group group = groupFactory.createGroup(groupName, false); // Create a maximum-length author String authorName = TestUtils.createRandomString(MAX_AUTHOR_NAME_LENGTH); @@ -177,13 +177,13 @@ public class ConstantsTest extends BriarTestCase { @Test public void testGroupsFitIntoSubscriptionUpdate() throws Exception { // Create the maximum number of maximum-length groups - Collection<Group> subs = new ArrayList<Group>(); + Collection<Group> groups = new ArrayList<Group>(); for(int i = 0; i < MAX_SUBSCRIPTIONS; i++) { String name = TestUtils.createRandomString(MAX_GROUP_NAME_LENGTH); - subs.add(groupFactory.createGroup(name)); + groups.add(groupFactory.createGroup(name, false)); } // Create a maximum-length subscription update - SubscriptionUpdate u = new SubscriptionUpdate(subs, Long.MAX_VALUE); + SubscriptionUpdate u = new SubscriptionUpdate(groups, Long.MAX_VALUE); // Serialise the update ByteArrayOutputStream out = new ByteArrayOutputStream(); PacketWriter writer = packetWriterFactory.createPacketWriter(out, true); diff --git a/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java b/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java index 5d0d325fb8..2511f6a594 100644 --- a/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java +++ b/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java @@ -1,6 +1,7 @@ package net.sf.briar.messaging.simplex; import static net.sf.briar.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static net.sf.briar.api.messaging.MessagingConstants.GROUP_SALT_LENGTH; import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; import java.io.ByteArrayInputStream; @@ -21,7 +22,9 @@ import net.sf.briar.api.crypto.KeyManager; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; +import net.sf.briar.api.db.event.MessageAddedEvent; +import net.sf.briar.api.messaging.Group; +import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageFactory; import net.sf.briar.api.messaging.MessageVerifier; @@ -57,6 +60,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { private final File testDir = TestUtils.getTestDirectory(); private final File aliceDir = new File(testDir, "alice"); private final File bobDir = new File(testDir, "bob"); + private final Group group; private final TransportId transportId; private final byte[] initialSecret; private final long epoch; @@ -64,6 +68,8 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { private Injector alice, bob; public SimplexMessagingIntegrationTest() throws Exception { + GroupId groupId = new GroupId(TestUtils.getRandomId()); + group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH], true); transportId = new TransportId(TestUtils.getRandomId()); // Create matching secrets for Alice and Bob initialSecret = new byte[32]; @@ -116,6 +122,9 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { Author bobAuthor = new Author(bobId, "Bob", new byte[MAX_PUBLIC_KEY_LENGTH]); ContactId contactId = db.addContact(bobAuthor, aliceId); + // Add the inbox group + db.addGroup(group); + db.setInboxGroup(contactId, group); // Add the transport and the endpoint db.addTransport(transportId, LATENCY); Endpoint ep = new Endpoint(contactId, transportId, epoch, true); @@ -126,9 +135,9 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { long timestamp = System.currentTimeMillis(); byte[] body = "Hi Bob!".getBytes("UTF-8"); MessageFactory messageFactory = alice.getInstance(MessageFactory.class); - Message message = messageFactory.createPrivateMessage(null, contentType, - timestamp, body); - db.addLocalPrivateMessage(message, contactId); + Message message = messageFactory.createAnonymousMessage(null, group, + contentType, timestamp, body); + db.addLocalMessage(message); // Create an outgoing simplex connection ByteArrayOutputStream out = new ByteArrayOutputStream(); ConnectionRegistry connRegistry = @@ -172,6 +181,9 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { Author aliceAuthor = new Author(aliceId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); ContactId contactId = db.addContact(aliceAuthor, bobId); + // Add the inbox group + db.addGroup(group); + db.setInboxGroup(contactId, group); // Add the transport and the endpoint db.addTransport(transportId, LATENCY); Endpoint ep = new Endpoint(contactId, transportId, epoch, false); @@ -227,7 +239,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { private boolean messageAdded = false; public void eventOccurred(DatabaseEvent e) { - if(e instanceof PrivateMessageAddedEvent) messageAdded = true; + if(e instanceof MessageAddedEvent) messageAdded = true; } } } -- GitLab