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.
This commit is contained in:
akwizgran
2013-12-19 21:53:26 +00:00
parent 1d4213e9c6
commit 0dc869228b
61 changed files with 1717 additions and 2329 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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. */

View File

@@ -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. */

View File

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

View File

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

View File

@@ -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.
@@ -112,6 +110,13 @@ interface Database<T> {
*/
void addLocalAuthor(T txn, LocalAuthor a) throws DbException;
/**
* Stores a message.
* <p>
* Locking: message write.
*/
void addMessage(T txn, Message m, boolean incoming) throws DbException;
/**
* Records a received message as needing to be acknowledged.
* <p>
@@ -119,15 +124,6 @@ interface Database<T> {
*/
void addMessageToAck(T txn, ContactId c, MessageId m) throws DbException;
/**
* Stores the given message, or returns false if the message is already in
* the database.
* <p>
* Locking: message write.
*/
boolean addPrivateMessage(T txn, Message m, ContactId c, boolean incoming)
throws DbException;
/**
* Stores the given temporary secrets and deletes any secrets that have
* been made obsolete.
@@ -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.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,7 +225,29 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
}
@Test
public void testGroupMessagesAreNotStoredUnlessSubscribed()
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() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(true));
oneOf(database).commitTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown);
db.addLocalMessage(message);
context.assertIsSatisfied();
}
@Test
public void testLocalMessagesAreNotStoredUnlessSubscribed()
throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
@@ -235,125 +255,52 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
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);
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 testDuplicateGroupMessagesAreNotStored() 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).commitTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown);
db.addLocalGroupMessage(message);
context.assertIsSatisfied();
}
@Test
public void testAddLocalGroupMessage() 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).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,82 +977,92 @@ 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);
will(returnValue(true));
oneOf(database).addStatus(txn, contactId, privateMessageId, true);
// The message must be acked
oneOf(database).addMessageToAck(txn, contactId, privateMessageId);
}});
DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown);
db.receiveMessage(contactId, privateMessage);
context.assertIsSatisfied();
}
@Test
public void testReceiveDuplicatePrivateMessage() 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));
// The message is not stored, it's a duplicate
oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
true);
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
// The message must still be acked
oneOf(database).addMessageToAck(txn, contactId, privateMessageId);
}});
DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown);
db.receiveMessage(contactId, privateMessage);
context.assertIsSatisfied();
}
@Test
public void testReceiveMessageDoesNotStoreGroupMessageUnlessSubscribed()
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);
oneOf(database).containsVisibleGroup(txn, contactId, groupId);
will(returnValue(true));
// Only store messages belonging to visible, subscribed groups
oneOf(database).containsVisibleSubscription(txn, contactId,
groupId);
will(returnValue(false));
// The message is not stored but it must still be acked
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.addListener(listener);
db.receiveMessage(contactId, message);
context.assertIsSatisfied();
}
@Test
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).containsContact(txn, contactId);
will(returnValue(true));
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.addListener(listener);
db.receiveMessage(contactId, message);
context.assertIsSatisfied();
}
@Test
public void testReceiveMessageWithoutVisibleGroup() 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).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
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);

View File

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

View File

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

View File

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