Merge branch '708-implement-protocol-for-private-group-messaging' into 'master'

Implement protocol for private group messaging

Closes #708

See merge request !360
This commit is contained in:
akwizgran
2016-10-31 15:17:51 +00:00
48 changed files with 1866 additions and 459 deletions

View File

@@ -61,7 +61,7 @@ import static org.briarproject.api.sync.ValidationManager.State.PENDING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class BlogManagerTest {
public class BlogManagerTest extends BriarIntegrationTest {
private LifecycleManager lifecycleManager0, lifecycleManager1;
private SyncSessionFactory sync0, sync1;
@@ -94,7 +94,7 @@ public class BlogManagerTest {
private final String AUTHOR2 = "Author 2";
private static final Logger LOG =
Logger.getLogger(ForumSharingIntegrationTest.class.getName());
Logger.getLogger(BlogManagerTest.class.getName());
private BlogManagerTestComponent t0, t1;

View File

@@ -0,0 +1,559 @@
package org.briarproject;
import net.jodah.concurrentunit.Waiter;
import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageStateChangedEvent;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.GroupMessage;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.privategroup.JoinMessageHeader;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.SyncSessionFactory;
import org.briarproject.api.system.Clock;
import org.briarproject.contact.ContactModule;
import org.briarproject.crypto.CryptoModule;
import org.briarproject.lifecycle.LifecycleModule;
import org.briarproject.privategroup.PrivateGroupModule;
import org.briarproject.properties.PropertiesModule;
import org.briarproject.sync.SyncModule;
import org.briarproject.transport.TransportModule;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.TestPluginsModule.MAX_LATENCY;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.api.sync.ValidationManager.State.PENDING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PrivateGroupManagerTest extends BriarIntegrationTest {
private LifecycleManager lifecycleManager0, lifecycleManager1;
private SyncSessionFactory sync0, sync1;
private PrivateGroupManager groupManager0, groupManager1;
private ContactManager contactManager0, contactManager1;
private ContactId contactId0, contactId1;
private IdentityManager identityManager0, identityManager1;
private LocalAuthor author0, author1;
private PrivateGroup privateGroup0;
private GroupId groupId0;
private GroupMessage newMemberMsg0;
@Inject
Clock clock;
@Inject
AuthorFactory authorFactory;
@Inject
CryptoComponent crypto;
@Inject
PrivateGroupFactory privateGroupFactory;
@Inject
GroupMessageFactory groupMessageFactory;
// objects accessed from background threads need to be volatile
private volatile Waiter validationWaiter;
private volatile Waiter deliveryWaiter;
private final File testDir = TestUtils.getTestDirectory();
private final SecretKey master = TestUtils.getSecretKey();
private final int TIMEOUT = 15000;
private final String AUTHOR1 = "Author 1";
private final String AUTHOR2 = "Author 2";
private static final Logger LOG =
Logger.getLogger(PrivateGroupManagerTest.class.getName());
private PrivateGroupManagerTestComponent t0, t1;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() throws Exception {
PrivateGroupManagerTestComponent component =
DaggerPrivateGroupManagerTestComponent.builder().build();
component.inject(this);
injectEagerSingletons(component);
assertTrue(testDir.mkdirs());
File t0Dir = new File(testDir, AUTHOR1);
t0 = DaggerPrivateGroupManagerTestComponent.builder()
.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
injectEagerSingletons(t0);
File t1Dir = new File(testDir, AUTHOR2);
t1 = DaggerPrivateGroupManagerTestComponent.builder()
.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
injectEagerSingletons(t1);
identityManager0 = t0.getIdentityManager();
identityManager1 = t1.getIdentityManager();
contactManager0 = t0.getContactManager();
contactManager1 = t1.getContactManager();
groupManager0 = t0.getPrivateGroupManager();
groupManager1 = t1.getPrivateGroupManager();
sync0 = t0.getSyncSessionFactory();
sync1 = t1.getSyncSessionFactory();
// initialize waiters fresh for each test
validationWaiter = new Waiter();
deliveryWaiter = new Waiter();
startLifecycles();
}
@Test
public void testSendingMessage() throws Exception {
defaultInit();
// create and add test message
long time = clock.currentTimeMillis();
String body = "This is a test message!";
MessageId previousMsgId =
groupManager0.getPreviousMsgId(groupId0);
GroupMessage msg = groupMessageFactory
.createGroupMessage(groupId0, time, null, author0, body,
previousMsgId);
groupManager0.addLocalMessage(msg);
assertEquals(msg.getMessage().getId(),
groupManager0.getPreviousMsgId(groupId0));
// sync test message
sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
// assert that message arrived as expected
Collection<GroupMessageHeader> headers =
groupManager1.getHeaders(groupId0);
assertEquals(3, headers.size());
GroupMessageHeader header = null;
for (GroupMessageHeader h : headers) {
if (!(h instanceof JoinMessageHeader)) {
header = h;
}
}
assertTrue(header != null);
assertFalse(header.isRead());
assertEquals(author0, header.getAuthor());
assertEquals(time, header.getTimestamp());
assertEquals(VERIFIED, header.getAuthorStatus());
assertEquals(body, groupManager1.getMessageBody(header.getId()));
GroupCount count = groupManager1.getGroupCount(groupId0);
assertEquals(2, count.getUnreadCount());
assertEquals(time, count.getLatestMsgTime());
assertEquals(3, count.getMsgCount());
}
@Test
public void testMessageWithWrongPreviousMsgId() throws Exception {
defaultInit();
// create and add test message with no previousMsgId
GroupMessage msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
author0, "test", null);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
// create and add test message with random previousMsgId
MessageId previousMsgId = new MessageId(TestUtils.getRandomId());
msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
author0, "test", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
// create and add test message with wrong previousMsgId
previousMsgId = groupManager1.getPreviousMsgId(groupId0);
msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
author0, "test", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
// create and add test message with previousMsgId of newMemberMsg
previousMsgId = newMemberMsg0.getMessage().getId();
msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
author0, "test", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
}
@Test
public void testMessageWithWrongParentMsgId() throws Exception {
defaultInit();
// create and add test message with random parentMsgId
MessageId parentMsgId = new MessageId(TestUtils.getRandomId());
MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
GroupMessage msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(),
parentMsgId, author0, "test", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
// create and add test message with wrong parentMsgId
parentMsgId = previousMsgId;
msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(),
parentMsgId, author0, "test", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
}
@Test
public void testMessageWithWrongTimestamp() throws Exception {
defaultInit();
// create and add test message with wrong timestamp
MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
GroupMessage msg = groupMessageFactory
.createGroupMessage(groupId0, 42, null, author0, "test",
previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
// create and add test message with good timestamp
long time = clock.currentTimeMillis();
msg = groupMessageFactory
.createGroupMessage(groupId0, time, null, author0, "test",
previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
assertEquals(3, groupManager1.getHeaders(groupId0).size());
// create and add test message with same timestamp as previous message
previousMsgId = msg.getMessage().getId();
msg = groupMessageFactory
.createGroupMessage(groupId0, time, previousMsgId, author0,
"test2", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(3, groupManager1.getHeaders(groupId0).size());
}
@Test
public void testWrongJoinMessages() throws Exception {
addDefaultIdentities();
addDefaultContacts();
listenToEvents();
// author0 joins privateGroup0 with later timestamp
long joinTime = clock.currentTimeMillis();
GroupMessage newMemberMsg = groupMessageFactory
.createNewMemberMessage(groupId0, joinTime, author0, author0);
GroupMessage joinMsg = groupMessageFactory
.createJoinMessage(groupId0, joinTime + 1, author0,
newMemberMsg.getMessage().getId());
groupManager0.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
assertEquals(joinMsg.getMessage().getId(),
groupManager0.getPreviousMsgId(groupId0));
// make group visible to 1
Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
t0.getDatabaseComponent()
.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
true);
txn0.setComplete();
t0.getDatabaseComponent().endTransaction(txn0);
// author1 joins privateGroup0 and refers to wrong NEW_MEMBER message
joinMsg = groupMessageFactory
.createJoinMessage(groupId0, joinTime, author1,
newMemberMsg.getMessage().getId());
joinTime = clock.currentTimeMillis();
newMemberMsg = groupMessageFactory
.createNewMemberMessage(groupId0, joinTime, author0, author1);
groupManager1.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
assertEquals(joinMsg.getMessage().getId(),
groupManager1.getPreviousMsgId(groupId0));
// make group visible to 0
Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
t1.getDatabaseComponent()
.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
true);
txn1.setComplete();
t1.getDatabaseComponent().endTransaction(txn1);
// sync join messages
sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
validationWaiter.await(TIMEOUT, 1);
// assert that 0 never joined the group from 1's perspective
assertEquals(1, groupManager1.getHeaders(groupId0).size());
sync1To0();
deliveryWaiter.await(TIMEOUT, 1);
validationWaiter.await(TIMEOUT, 1);
// assert that 1 never joined the group from 0's perspective
assertEquals(1, groupManager0.getHeaders(groupId0).size());
}
@After
public void tearDown() throws Exception {
stopLifecycles();
TestUtils.deleteTestDirectory(testDir);
}
private class Listener implements EventListener {
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
if (!event.isLocal()) {
if (event.getState() == DELIVERED) {
LOG.info("Delivered new message");
deliveryWaiter.resume();
} else if (event.getState() == INVALID ||
event.getState() == PENDING) {
LOG.info("Validated new " + event.getState().name() +
" message");
validationWaiter.resume();
}
}
}
}
}
private void defaultInit() throws Exception {
addDefaultIdentities();
addDefaultContacts();
listenToEvents();
addGroup();
}
private void addDefaultIdentities() throws DbException {
KeyPair keyPair0 = crypto.generateSignatureKeyPair();
byte[] publicKey0 = keyPair0.getPublic().getEncoded();
byte[] privateKey0 = keyPair0.getPrivate().getEncoded();
author0 = authorFactory
.createLocalAuthor(AUTHOR1, publicKey0, privateKey0);
identityManager0.addLocalAuthor(author0);
privateGroup0 =
privateGroupFactory.createPrivateGroup("Testgroup", author0);
groupId0 = privateGroup0.getId();
KeyPair keyPair1 = crypto.generateSignatureKeyPair();
byte[] publicKey1 = keyPair1.getPublic().getEncoded();
byte[] privateKey1 = keyPair1.getPrivate().getEncoded();
author1 = authorFactory
.createLocalAuthor(AUTHOR2, publicKey1, privateKey1);
identityManager1.addLocalAuthor(author1);
}
private void addDefaultContacts() throws DbException {
// sharer adds invitee as contact
contactId1 = contactManager0.addContact(author1,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
// invitee adds sharer back
contactId0 = contactManager1.addContact(author0,
author1.getId(), master, clock.currentTimeMillis(), true,
true, true
);
}
private void listenToEvents() {
Listener listener0 = new Listener();
t0.getEventBus().addListener(listener0);
Listener listener1 = new Listener();
t1.getEventBus().addListener(listener1);
}
private void addGroup() throws Exception {
// author0 joins privateGroup0
long joinTime = clock.currentTimeMillis();
newMemberMsg0 = groupMessageFactory
.createNewMemberMessage(privateGroup0.getId(), joinTime,
author0, author0);
GroupMessage joinMsg = groupMessageFactory
.createJoinMessage(privateGroup0.getId(), joinTime, author0,
newMemberMsg0.getMessage().getId());
groupManager0.addPrivateGroup(privateGroup0, newMemberMsg0, joinMsg);
assertEquals(joinMsg.getMessage().getId(),
groupManager0.getPreviousMsgId(groupId0));
// make group visible to 1
Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
t0.getDatabaseComponent()
.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
true);
txn0.setComplete();
t0.getDatabaseComponent().endTransaction(txn0);
// author1 joins privateGroup0
joinTime = clock.currentTimeMillis();
GroupMessage newMemberMsg1 = groupMessageFactory
.createNewMemberMessage(privateGroup0.getId(), joinTime,
author0, author1);
joinMsg = groupMessageFactory
.createJoinMessage(privateGroup0.getId(), joinTime, author1,
newMemberMsg1.getMessage().getId());
groupManager1.addPrivateGroup(privateGroup0, newMemberMsg1, joinMsg);
assertEquals(joinMsg.getMessage().getId(),
groupManager1.getPreviousMsgId(groupId0));
// make group visible to 0
Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
t1.getDatabaseComponent()
.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
true);
txn1.setComplete();
t1.getDatabaseComponent().endTransaction(txn1);
// sync join messages
sync0To1();
deliveryWaiter.await(TIMEOUT, 2);
sync1To0();
deliveryWaiter.await(TIMEOUT, 2);
}
private void sync0To1() throws IOException, TimeoutException {
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
}
private void sync1To0() throws IOException, TimeoutException {
deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
}
private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
SyncSessionFactory toSync, ContactId toId, String debug)
throws IOException, TimeoutException {
if (debug != null) LOG.info("TEST: Sending message from " + debug);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Create an outgoing sync session
SyncSession sessionFrom =
fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
// Write whatever needs to be written
sessionFrom.run();
out.close();
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
// Create an incoming sync session
SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
// Read whatever needs to be read
sessionTo.run();
in.close();
}
private void startLifecycles() throws InterruptedException {
// Start the lifecycle manager and wait for it to finish
lifecycleManager0 = t0.getLifecycleManager();
lifecycleManager1 = t1.getLifecycleManager();
lifecycleManager0.startServices();
lifecycleManager1.startServices();
lifecycleManager0.waitForStartup();
lifecycleManager1.waitForStartup();
}
private void stopLifecycles() throws InterruptedException {
// Clean up
lifecycleManager0.stopServices();
lifecycleManager1.stopServices();
lifecycleManager0.waitForShutdown();
lifecycleManager1.waitForShutdown();
}
private void injectEagerSingletons(
PrivateGroupManagerTestComponent component) {
component.inject(new LifecycleModule.EagerSingletons());
component.inject(new PrivateGroupModule.EagerSingletons());
component.inject(new CryptoModule.EagerSingletons());
component.inject(new ContactModule.EagerSingletons());
component.inject(new TransportModule.EagerSingletons());
component.inject(new SyncModule.EagerSingletons());
component.inject(new PropertiesModule.EagerSingletons());
}
}

View File

@@ -0,0 +1,83 @@
package org.briarproject;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.sync.SyncSessionFactory;
import org.briarproject.clients.ClientsModule;
import org.briarproject.contact.ContactModule;
import org.briarproject.crypto.CryptoModule;
import org.briarproject.data.DataModule;
import org.briarproject.db.DatabaseModule;
import org.briarproject.event.EventModule;
import org.briarproject.identity.IdentityModule;
import org.briarproject.lifecycle.LifecycleModule;
import org.briarproject.messaging.MessagingModule;
import org.briarproject.privategroup.PrivateGroupModule;
import org.briarproject.properties.PropertiesModule;
import org.briarproject.sharing.SharingModule;
import org.briarproject.sync.SyncModule;
import org.briarproject.system.SystemModule;
import org.briarproject.transport.TransportModule;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
TestDatabaseModule.class,
TestPluginsModule.class,
TestSeedProviderModule.class,
ClientsModule.class,
ContactModule.class,
CryptoModule.class,
DataModule.class,
DatabaseModule.class,
EventModule.class,
MessagingModule.class,
PrivateGroupModule.class,
IdentityModule.class,
LifecycleModule.class,
PropertiesModule.class,
SharingModule.class,
SyncModule.class,
SystemModule.class,
TransportModule.class
})
interface PrivateGroupManagerTestComponent {
void inject(PrivateGroupManagerTest testCase);
void inject(ContactModule.EagerSingletons init);
void inject(CryptoModule.EagerSingletons init);
void inject(PrivateGroupModule.EagerSingletons init);
void inject(LifecycleModule.EagerSingletons init);
void inject(PropertiesModule.EagerSingletons init);
void inject(SyncModule.EagerSingletons init);
void inject(TransportModule.EagerSingletons init);
LifecycleManager getLifecycleManager();
EventBus getEventBus();
IdentityManager getIdentityManager();
ContactManager getContactManager();
PrivateGroupManager getPrivateGroupManager();
SyncSessionFactory getSyncSessionFactory();
DatabaseComponent getDatabaseComponent();
}

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/forum_cell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
android:baselineAligned="false">
<RelativeLayout
android:layout_width="wrap_content"

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_medium"
android:baselineAligned="false"
android:orientation="vertical">
<View
android:id="@+id/top_divider"
style="@style/Divider.ForumList"
android:layout_width="match_parent"
android:layout_height="@dimen/margin_separator"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_small"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_marginRight="@dimen/margin_medium"
android:layout_marginTop="@dimen/margin_medium"
android:orientation="horizontal">
<org.briarproject.android.view.AuthorView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:persona="commenter"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/margin_medium"
android:gravity="center_vertical"
android:textColor="@color/briar_text_secondary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
tools:text="@string/groups_member_joined"/>
</LinearLayout>
</LinearLayout>

View File

@@ -166,6 +166,7 @@
<string name="groups_invite_members">Invite Members</string>
<string name="groups_leave">Leave Group</string>
<string name="groups_dissolve">Dissolve Group</string>
<string name="groups_member_joined">joined the group.</string>
<!-- Private Group Invitations -->
<string name="groups_invitations_title">Group Invitations</string>

View File

@@ -120,6 +120,7 @@ public class ActivityModule {
@Provides
protected GroupController provideGroupController(
GroupControllerImpl groupController) {
activity.addLifecycleController(groupController);
return groupController;
}

View File

@@ -34,6 +34,8 @@ import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.plugins.PluginManager;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.settings.SettingsManager;
@@ -99,6 +101,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
GroupInvitationManager groupInvitationManager();
PrivateGroupFactory privateGroupFactory();
GroupMessageFactory groupMessageFactory();
ForumManager forumManager();
ForumSharingManager forumSharingManager();

View File

@@ -18,8 +18,9 @@ import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.sharing.ShareForumActivity;
import org.briarproject.android.sharing.ForumSharingStatusActivity;
import org.briarproject.android.sharing.ShareForumActivity;
import org.briarproject.android.threaded.ThreadItemAdapter;
import org.briarproject.android.threaded.ThreadListActivity;
import org.briarproject.android.threaded.ThreadListController;
import org.briarproject.api.db.DbException;
@@ -35,7 +36,7 @@ import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
public class ForumActivity extends
ThreadListActivity<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> {
ThreadListActivity<Forum, ForumItem, ForumPostHeader> {
private static final int REQUEST_FORUM_SHARED = 3;
@@ -74,9 +75,9 @@ public class ForumActivity extends
}
@Override
protected NestedForumAdapter createAdapter(
protected ThreadItemAdapter<ForumItem> createAdapter(
LinearLayoutManager layoutManager) {
return new NestedForumAdapter(this, layoutManager);
return new ThreadItemAdapter<>(this, layoutManager);
}
@Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
import android.support.annotation.Nullable;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.android.threaded.ThreadListControllerImpl;
import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.crypto.CryptoExecutor;
@@ -28,8 +29,11 @@ import java.util.logging.Logger;
import javax.inject.Inject;
public class ForumControllerImpl extends
ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
import static java.lang.Math.max;
import static java.util.logging.Level.WARNING;
public class ForumControllerImpl
extends ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
implements ForumController {
private static final Logger LOG =
@@ -42,9 +46,9 @@ public class ForumControllerImpl extends
LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor,
ForumManager forumManager, EventBus eventBus,
AndroidNotificationManager notificationManager, Clock clock) {
Clock clock, AndroidNotificationManager notificationManager) {
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
eventBus, notificationManager, clock);
eventBus, clock, notificationManager);
this.forumManager = forumManager;
}
@@ -84,8 +88,8 @@ public class ForumControllerImpl extends
}
@Override
protected String loadMessageBody(MessageId id) throws DbException {
return StringUtils.fromUtf8(forumManager.getPostBody(id));
protected String loadMessageBody(ForumPostHeader h) throws DbException {
return StringUtils.fromUtf8(forumManager.getPostBody(h.getId()));
}
@Override
@@ -94,16 +98,42 @@ public class ForumControllerImpl extends
}
@Override
protected long getLatestTimestamp() throws DbException {
GroupCount count = forumManager.getGroupCount(getGroupId());
return count.getLatestMsgTime();
public void createAndStoreMessage(final String body,
@Nullable final ForumItem parentItem,
final ResultExceptionHandler<ForumItem, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
LocalAuthor author = identityManager.getLocalAuthor();
GroupCount count = forumManager.getGroupCount(getGroupId());
long timestamp = max(count.getLatestMsgTime() + 1,
clock.currentTimeMillis());
MessageId parentId = parentItem != null ?
parentItem.getId() : null;
createMessage(body, timestamp, parentId, author, handler);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
protected ForumPost createLocalMessage(String body, long timestamp,
@Nullable MessageId parentId, LocalAuthor author) {
return forumManager.createLocalPost(getGroupId(), body, timestamp,
parentId, author);
private void createMessage(final String body, final long timestamp,
final @Nullable MessageId parentId, final LocalAuthor author,
final ResultExceptionHandler<ForumItem, DbException> handler) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
LOG.info("Creating forum post...");
ForumPost msg = forumManager
.createLocalPost(getGroupId(), body, timestamp,
parentId, author);
storePost(msg, body, handler);
}
});
}
@Override

View File

@@ -1,28 +0,0 @@
package org.briarproject.android.forum;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.threaded.ThreadItemAdapter;
@UiThread
class NestedForumAdapter extends ThreadItemAdapter<ForumItem> {
NestedForumAdapter(ThreadItemListener<ForumItem> listener,
LinearLayoutManager layoutManager) {
super(listener, layoutManager);
}
@Override
public NestedForumHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_forum_post, parent, false);
return new NestedForumHolder(v);
}
}

View File

@@ -1,13 +0,0 @@
package org.briarproject.android.forum;
import android.view.View;
import org.briarproject.android.threaded.ThreadItemViewHolder;
public class NestedForumHolder extends ThreadItemViewHolder<ForumItem> {
public NestedForumHolder(View v) {
super(v);
}
}

View File

@@ -22,7 +22,7 @@ import javax.inject.Inject;
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
public class GroupActivity extends
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessageAdapter> {
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
@Inject
GroupController controller;

View File

@@ -3,8 +3,8 @@ package org.briarproject.android.privategroup.conversation;
import android.support.annotation.Nullable;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.android.threaded.ThreadListControllerImpl;
import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
@@ -15,7 +15,9 @@ import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.GroupMessage;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.privategroup.JoinMessageHeader;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.sync.MessageId;
@@ -27,6 +29,9 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.lang.Math.max;
import static java.util.logging.Level.WARNING;
public class GroupControllerImpl extends
ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage>
implements GroupController {
@@ -35,16 +40,19 @@ public class GroupControllerImpl extends
Logger.getLogger(GroupControllerImpl.class.getName());
private final PrivateGroupManager privateGroupManager;
private final GroupMessageFactory groupMessageFactory;
@Inject
GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor,
PrivateGroupManager privateGroupManager, EventBus eventBus,
AndroidNotificationManager notificationManager, Clock clock) {
PrivateGroupManager privateGroupManager,
GroupMessageFactory groupMessageFactory, EventBus eventBus,
Clock clock, AndroidNotificationManager notificationManager) {
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
eventBus, notificationManager, clock);
eventBus, clock, notificationManager);
this.privateGroupManager = privateGroupManager;
this.groupMessageFactory = groupMessageFactory;
}
@Override
@@ -83,8 +91,13 @@ public class GroupControllerImpl extends
}
@Override
protected String loadMessageBody(MessageId id) throws DbException {
return privateGroupManager.getMessageBody(id);
protected String loadMessageBody(GroupMessageHeader header)
throws DbException {
if (header instanceof JoinMessageHeader) {
// will be looked up later
return "";
}
return privateGroupManager.getMessageBody(header.getId());
}
@Override
@@ -93,16 +106,52 @@ public class GroupControllerImpl extends
}
@Override
protected long getLatestTimestamp() throws DbException {
GroupCount count = privateGroupManager.getGroupCount(getGroupId());
return count.getLatestMsgTime();
public void createAndStoreMessage(final String body,
@Nullable final GroupMessageItem parentItem,
final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
LocalAuthor author = identityManager.getLocalAuthor();
MessageId parentId = null;
MessageId previousMsgId =
privateGroupManager.getPreviousMsgId(getGroupId());
// timestamp must be greater than the timestamps
// of the member's previous message...
long timestamp = privateGroupManager
.getMessageTimestamp(previousMsgId);
// ...and the parent post, if any
if (parentItem != null) {
timestamp = max(parentItem.getTimestamp(), timestamp);
parentId = parentItem.getId();
}
timestamp = max(clock.currentTimeMillis(), timestamp + 1);
createMessage(body, timestamp, parentId, author,
previousMsgId, handler);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
protected GroupMessage createLocalMessage(String body, long timestamp,
@Nullable MessageId parentId, LocalAuthor author) {
return privateGroupManager.createLocalMessage(getGroupId(), body,
timestamp, parentId, author);
private void createMessage(final String body, final long timestamp,
final @Nullable MessageId parentId, final LocalAuthor author,
final MessageId previousMsgId,
final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
LOG.info("Creating group message...");
GroupMessage msg = groupMessageFactory
.createGroupMessage(getGroupId(), timestamp,
parentId, author, body, previousMsgId);
storePost(msg, body, handler);
}
});
}
@Override
@@ -119,6 +168,9 @@ public class GroupControllerImpl extends
@Override
protected GroupMessageItem buildItem(GroupMessageHeader header,
String body) {
if (header instanceof JoinMessageHeader) {
return new JoinMessageItem(header, body);
}
return new GroupMessageItem(header, body);
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.android.privategroup.conversation;
import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
@@ -7,7 +8,9 @@ import android.view.View;
import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.threaded.BaseThreadItemViewHolder;
import org.briarproject.android.threaded.ThreadItemAdapter;
import org.briarproject.android.threaded.ThreadPostViewHolder;
@UiThread
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
@@ -17,12 +20,23 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
super(listener, layoutManager);
}
@LayoutRes
@Override
public GroupMessageViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
public int getItemViewType(int position) {
GroupMessageItem item = getVisibleItem(position);
if (item != null) return item.getLayout();
return R.layout.list_item_thread;
}
@Override
public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
ViewGroup parent, int type) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_forum_post, parent, false);
return new GroupMessageViewHolder(v);
.inflate(type, parent, false);
if (type == R.layout.list_item_thread_notice) {
return new JoinMessageItemViewHolder(v);
}
return new ThreadPostViewHolder<>(v);
}
}

View File

@@ -1,14 +1,22 @@
package org.briarproject.android.privategroup.conversation;
import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread;
import org.briarproject.R;
import org.briarproject.android.threaded.ThreadItem;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.sync.MessageId;
import javax.annotation.concurrent.NotThreadSafe;
@UiThread
@NotThreadSafe
class GroupMessageItem extends ThreadItem {
GroupMessageItem(MessageId messageId, MessageId parentId,
private GroupMessageItem(MessageId messageId, MessageId parentId,
String text, long timestamp, Author author, Status status,
boolean isRead) {
super(messageId, parentId, text, timestamp, author, status, isRead);
@@ -19,4 +27,9 @@ class GroupMessageItem extends ThreadItem {
h.getAuthorStatus(), h.isRead());
}
@LayoutRes
public int getLayout() {
return R.layout.list_item_thread;
}
}

View File

@@ -1,14 +0,0 @@
package org.briarproject.android.privategroup.conversation;
import android.view.View;
import org.briarproject.android.threaded.ThreadItemViewHolder;
public class GroupMessageViewHolder
extends ThreadItemViewHolder<GroupMessageItem> {
public GroupMessageViewHolder(View v) {
super(v);
}
}

View File

@@ -0,0 +1,35 @@
package org.briarproject.android.privategroup.conversation;
import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread;
import org.briarproject.R;
import org.briarproject.api.privategroup.GroupMessageHeader;
import javax.annotation.concurrent.NotThreadSafe;
@UiThread
@NotThreadSafe
class JoinMessageItem extends GroupMessageItem {
JoinMessageItem(GroupMessageHeader h,
String text) {
super(h, text);
}
@Override
public int getLevel() {
return 0;
}
@Override
public boolean hasDescendants() {
return false;
}
@LayoutRes
public int getLayout() {
return R.layout.list_item_thread_notice;
}
}

View File

@@ -0,0 +1,30 @@
package org.briarproject.android.privategroup.conversation;
import android.support.annotation.UiThread;
import android.view.View;
import org.briarproject.R;
import org.briarproject.android.threaded.BaseThreadItemViewHolder;
import org.briarproject.android.threaded.ThreadItemAdapter;
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.api.nullsafety.NotNullByDefault;
@UiThread
@NotNullByDefault
public class JoinMessageItemViewHolder
extends BaseThreadItemViewHolder<GroupMessageItem> {
public JoinMessageItemViewHolder(View v) {
super(v);
}
@Override
public void bind(final ThreadItemAdapter<GroupMessageItem> adapter,
final ThreadItemListener<GroupMessageItem> listener,
final GroupMessageItem item, int pos) {
super.bind(adapter, listener, item, pos);
textView.setText(getContext().getString(R.string.groups_member_joined));
}
}

View File

@@ -3,11 +3,19 @@ package org.briarproject.android.privategroup.creation;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.GroupMessage;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.system.Clock;
import java.util.Collection;
import java.util.concurrent.Executor;
@@ -23,25 +31,81 @@ public class CreateGroupControllerImpl extends DbControllerImpl
private static final Logger LOG =
Logger.getLogger(CreateGroupControllerImpl.class.getName());
private final IdentityManager identityManager;
private final PrivateGroupFactory groupFactory;
private final GroupMessageFactory groupMessageFactory;
private final PrivateGroupManager groupManager;
private final Clock clock;
@CryptoExecutor
private final Executor cryptoExecutor;
@Inject
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
PrivateGroupManager groupManager) {
@CryptoExecutor Executor cryptoExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager,
PrivateGroupFactory groupFactory,
GroupMessageFactory groupMessageFactory,
PrivateGroupManager groupManager, Clock clock) {
super(dbExecutor, lifecycleManager);
this.identityManager = identityManager;
this.groupFactory = groupFactory;
this.groupMessageFactory = groupMessageFactory;
this.groupManager = groupManager;
this.clock = clock;
this.cryptoExecutor = cryptoExecutor;
}
@Override
public void createGroup(final String name,
final ResultExceptionHandler<GroupId, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
LocalAuthor author = identityManager.getLocalAuthor();
createGroupAndMessages(author, name, handler);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
private void createGroupAndMessages(final LocalAuthor author,
final String name,
final ResultExceptionHandler<GroupId, DbException> handler) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
LOG.info("Creating group...");
PrivateGroup group =
groupFactory.createPrivateGroup(name, author);
LOG.info("Creating new member announcement...");
GroupMessage newMemberMsg = groupMessageFactory
.createNewMemberMessage(group.getId(),
clock.currentTimeMillis(), author, author);
LOG.info("Creating new join announcement...");
GroupMessage joinMsg = groupMessageFactory
.createJoinMessage(group.getId(),
newMemberMsg.getMessage().getTimestamp(),
author, newMemberMsg.getMessage().getId());
storeGroup(group, newMemberMsg, joinMsg, handler);
}
});
}
private void storeGroup(final PrivateGroup group,
final GroupMessage newMemberMsg, final GroupMessage joinMsg,
final ResultExceptionHandler<GroupId, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
LOG.info("Adding group to database...");
try {
handler.onResult(groupManager.addPrivateGroup(name));
groupManager.addPrivateGroup(group, newMemberMsg, joinMsg);
handler.onResult(group.getId());
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);

View File

@@ -5,6 +5,7 @@ import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
@@ -15,47 +16,32 @@ import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.android.view.AuthorView;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.util.StringUtils;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@UiThread
public abstract class ThreadItemViewHolder<I extends ThreadItem>
@NotNullByDefault
public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
extends RecyclerView.ViewHolder {
private final static int ANIMATION_DURATION = 5000;
private final TextView textView, lvlText, repliesText;
protected final TextView textView;
private final ViewGroup layout;
private final AuthorView author;
private final View[] lvls;
private final View chevron, replyButton;
private final ViewGroup cell;
private final View topDivider;
public ThreadItemViewHolder(View v) {
public BaseThreadItemViewHolder(View v) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.layout);
textView = (TextView) v.findViewById(R.id.text);
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
author = (AuthorView) v.findViewById(R.id.author);
repliesText = (TextView) v.findViewById(R.id.replies);
int[] nestedLineIds = {
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
R.id.nested_line_4, R.id.nested_line_5
};
lvls = new View[nestedLineIds.length];
for (int i = 0; i < lvls.length; i++) {
lvls[i] = v.findViewById(nestedLineIds[i]);
}
chevron = v.findViewById(R.id.chevron);
replyButton = v.findViewById(R.id.btn_reply);
cell = (ViewGroup) v.findViewById(R.id.forum_cell);
topDivider = v.findViewById(R.id.top_divider);
}
// TODO improve encapsulation, so we don't need to pass the adapter here
@CallSuper
public void bind(final ThreadItemAdapter<I> adapter,
final ThreadItemListener<I> listener, final I item, int pos) {
@@ -67,68 +53,22 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
topDivider.setVisibility(View.VISIBLE);
}
for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
}
if (item.getLevel() > 5) {
lvlText.setVisibility(VISIBLE);
lvlText.setText("" + item.getLevel());
} else {
lvlText.setVisibility(GONE);
}
author.setAuthor(item.getAuthor());
author.setDate(item.getTimestamp());
author.setAuthorStatus(item.getStatus());
int replies = adapter.getReplyCount(item);
if (replies == 0) {
repliesText.setText("");
} else {
repliesText.setText(getContext().getResources()
.getQuantityString(R.plurals.message_replies, replies,
replies));
}
if (item.hasDescendants()) {
chevron.setVisibility(VISIBLE);
if (item.isShowingDescendants()) {
chevron.setSelected(false);
} else {
chevron.setSelected(true);
}
chevron.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
chevron.setSelected(!chevron.isSelected());
if (chevron.isSelected()) {
adapter.hideDescendants(item);
} else {
adapter.showDescendants(item);
}
}
});
} else {
chevron.setVisibility(INVISIBLE);
}
if (item.equals(adapter.getReplyItem())) {
cell.setBackgroundColor(ContextCompat
layout.setBackgroundColor(ContextCompat
.getColor(getContext(), R.color.forum_cell_highlight));
} else if (item.equals(adapter.getAddedItem())) {
cell.setBackgroundColor(ContextCompat
layout.setBackgroundColor(ContextCompat
.getColor(getContext(), R.color.forum_cell_highlight));
animateFadeOut(adapter, adapter.getAddedItem());
adapter.clearAddedItem();
} else {
cell.setBackgroundColor(ContextCompat
layout.setBackgroundColor(ContextCompat
.getColor(getContext(), R.color.window_background));
}
replyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onReplyClick(item);
adapter.scrollTo(item);
}
});
}
private void animateFadeOut(final ThreadItemAdapter<I> adapter,
@@ -137,7 +77,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator();
adapter.addAnimatingItem(addedItem, anim);
ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
ColorDrawable viewColor = (ColorDrawable) layout.getBackground();
anim.setIntValues(viewColor.getColor(), ContextCompat
.getColor(getContext(), R.color.window_background));
anim.setEvaluator(new ArgbEvaluator());
@@ -167,7 +107,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
cell.setBackgroundColor(
layout.setBackgroundColor(
(Integer) valueAnimator.getAnimatedValue());
}
});
@@ -175,7 +115,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
anim.start();
}
private Context getContext() {
protected Context getContext() {
return textView.getContext();
}

View File

@@ -5,9 +5,11 @@ import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.sync.MessageId;
import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
/* This class is not thread safe */
@NotThreadSafe
public abstract class ThreadItem implements MessageNode {
private final MessageId messageId;
@@ -92,4 +94,5 @@ public abstract class ThreadItem implements MessageNode {
public void setDescendantCount(int descendantCount) {
this.descendantCount = descendantCount;
}
}

View File

@@ -5,7 +5,11 @@ import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.util.VersionedAdapter;
import org.briarproject.api.sync.MessageId;
@@ -17,8 +21,8 @@ import java.util.Map;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
public abstract class ThreadItemAdapter<I extends ThreadItem>
extends RecyclerView.Adapter<ThreadItemViewHolder<I>>
public class ThreadItemAdapter<I extends ThreadItem>
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
implements VersionedAdapter {
static final int UNDEFINED = -1;
@@ -42,7 +46,15 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
}
@Override
public void onBindViewHolder(ThreadItemViewHolder<I> ui, int position) {
public BaseThreadItemViewHolder<I> onCreateViewHolder(
ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_thread, parent, false);
return new ThreadPostViewHolder<>(v);
}
@Override
public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) {
I item = getVisibleItem(position);
if (item == null) return;
listener.onItemVisible(item);
@@ -304,7 +316,7 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
revision++;
}
protected interface ThreadItemListener<I> {
public interface ThreadItemListener<I> {
void onItemVisible(I item);

View File

@@ -35,7 +35,7 @@ import static android.support.design.widget.Snackbar.make;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
extends BriarActivity
implements ThreadListListener<H>, TextInputListener,
ThreadItemListener<I> {
@@ -46,7 +46,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
private static final Logger LOG =
Logger.getLogger(ThreadListActivity.class.getName());
protected A adapter;
protected ThreadItemAdapter<I> adapter;
protected BriarRecyclerView list;
protected TextInputView textInput;
protected GroupId groupId;
@@ -88,7 +88,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@LayoutRes
protected abstract int getLayout();
protected abstract A createAdapter(LinearLayoutManager layoutManager);
protected abstract ThreadItemAdapter<I> createAdapter(
LinearLayoutManager layoutManager);
protected void loadNamedGroup() {
getController().loadNamedGroup(
@@ -249,8 +250,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
finish();
}
};
getController().createAndStoreMessage(text,
replyItem != null ? replyItem.getId() : null, handler);
getController().createAndStoreMessage(text, replyItem, handler);
textInput.hideSoftKeyboard();
textInput.setVisibility(GONE);
textInput.setText("");

View File

@@ -10,7 +10,6 @@ import org.briarproject.api.clients.NamedGroup;
import org.briarproject.api.clients.PostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.Collection;
@@ -29,7 +28,7 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
void markItemsRead(Collection<I> items);
void createAndStoreMessage(String body, @Nullable MessageId parentId,
void createAndStoreMessage(String body, @Nullable I parentItem,
ResultExceptionHandler<I, DbException> handler);
void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler);

View File

@@ -2,7 +2,6 @@ package org.briarproject.android.threaded;
import android.app.Activity;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.DbControllerImpl;
@@ -18,7 +17,6 @@ import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
@@ -43,28 +41,28 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
private static final Logger LOG =
Logger.getLogger(ThreadListControllerImpl.class.getName());
private final IdentityManager identityManager;
private final Executor cryptoExecutor;
protected final IdentityManager identityManager;
protected final Executor cryptoExecutor;
protected final AndroidNotificationManager notificationManager;
protected final Clock clock;
private final EventBus eventBus;
private final Clock clock;
private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
private volatile GroupId groupId;
protected ThreadListListener<H> listener;
protected volatile ThreadListListener<H> listener;
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
AndroidNotificationManager notificationManager, Clock clock) {
Clock clock, AndroidNotificationManager notificationManager) {
super(dbExecutor, lifecycleManager);
this.identityManager = identityManager;
this.cryptoExecutor = cryptoExecutor;
this.eventBus = eventBus;
this.notificationManager = notificationManager;
this.clock = clock;
this.eventBus = eventBus;
}
@Override
@@ -160,7 +158,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
for (H header : headers) {
if (!bodyCache.containsKey(header.getId())) {
bodyCache.put(header.getId(),
loadMessageBody(header.getId()));
loadMessageBody(header));
}
}
duration = System.currentTimeMillis() - now;
@@ -182,7 +180,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
protected abstract Collection<H> loadHeaders() throws DbException;
@DatabaseExecutor
protected abstract String loadMessageBody(MessageId id) throws DbException;
protected abstract String loadMessageBody(H header) throws DbException;
@Override
public void loadItem(final H header,
@@ -194,7 +192,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
long now = System.currentTimeMillis();
String body;
if (!bodyCache.containsKey(header.getId())) {
body = loadMessageBody(header.getId());
body = loadMessageBody(header);
bodyCache.put(header.getId(), body);
} else {
body = bodyCache.get(header.getId());
@@ -242,57 +240,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
@DatabaseExecutor
protected abstract void markRead(MessageId id) throws DbException;
@Override
public void createAndStoreMessage(final String body,
@Nullable final MessageId parentId,
final ResultExceptionHandler<I, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
LocalAuthor author = identityManager.getLocalAuthor();
long timestamp = getLatestTimestamp();
timestamp = Math.max(timestamp, clock.currentTimeMillis());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) {
LOG.info("Loading identity and timestamp took " +
duration + " ms");
}
createMessage(body, timestamp, parentId, author, handler);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@DatabaseExecutor
protected abstract long getLatestTimestamp() throws DbException;
private void createMessage(final String body, final long timestamp,
final @Nullable MessageId parentId, final LocalAuthor author,
final ResultExceptionHandler<I, DbException> handler) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
long now = System.currentTimeMillis();
M msg = createLocalMessage(body, timestamp, parentId, author);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Creating message took " + duration + " ms");
storePost(msg, body, handler);
}
});
}
@CryptoExecutor
protected abstract M createLocalMessage(String body, long timestamp,
@Nullable MessageId parentId, LocalAuthor author);
private void storePost(final M msg, final String body,
protected void storePost(final M msg, final String body,
final ResultExceptionHandler<I, DbException> resultHandler) {
runOnDbThread(new Runnable() {
@Override

View File

@@ -0,0 +1,96 @@
package org.briarproject.android.threaded;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.api.nullsafety.NotNullByDefault;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@UiThread
@NotNullByDefault
public class ThreadPostViewHolder<I extends ThreadItem>
extends BaseThreadItemViewHolder<I> {
private final TextView lvlText, repliesText;
private final View[] lvls;
private final View chevron, replyButton;
public ThreadPostViewHolder(View v) {
super(v);
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
repliesText = (TextView) v.findViewById(R.id.replies);
int[] nestedLineIds = {
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
R.id.nested_line_4, R.id.nested_line_5
};
lvls = new View[nestedLineIds.length];
for (int i = 0; i < lvls.length; i++) {
lvls[i] = v.findViewById(nestedLineIds[i]);
}
chevron = v.findViewById(R.id.chevron);
replyButton = v.findViewById(R.id.btn_reply);
}
// TODO improve encapsulation, so we don't need to pass the adapter here
@Override
public void bind(final ThreadItemAdapter<I> adapter,
final ThreadItemListener<I> listener, final I item, int pos) {
super.bind(adapter, listener, item, pos);
for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
}
if (item.getLevel() > 5) {
lvlText.setVisibility(VISIBLE);
lvlText.setText("" + item.getLevel());
} else {
lvlText.setVisibility(GONE);
}
int replies = adapter.getReplyCount(item);
if (replies == 0) {
repliesText.setText("");
} else {
repliesText.setText(getContext().getResources()
.getQuantityString(R.plurals.message_replies, replies,
replies));
}
if (item.hasDescendants()) {
chevron.setVisibility(VISIBLE);
if (item.isShowingDescendants()) {
chevron.setSelected(false);
} else {
chevron.setSelected(true);
}
chevron.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
chevron.setSelected(!chevron.isSelected());
if (chevron.isSelected()) {
adapter.hideDescendants(item);
} else {
adapter.showDescendants(item);
}
}
});
} else {
chevron.setVisibility(INVISIBLE);
}
replyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onReplyClick(item);
adapter.scrollTo(item);
}
});
}
}

View File

@@ -8,6 +8,7 @@ import org.briarproject.BuildConfig;
import org.briarproject.TestUtils;
import org.briarproject.android.TestBriarApplication;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.threaded.ThreadItemAdapter;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
@@ -111,7 +112,7 @@ public class ForumActivityTest {
List<ForumItem> dummyData = getDummyData();
verify(mc, times(1)).loadItems(rc.capture());
rc.getValue().onResult(dummyData);
NestedForumAdapter adapter = forumActivity.getAdapter();
ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();
Assert.assertNotNull(adapter);
// Cascade close
assertEquals(6, adapter.getItemCount());

View File

@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
import org.briarproject.android.ActivityModule;
import org.briarproject.android.controller.BriarController;
import org.briarproject.android.controller.BriarControllerImpl;
import org.briarproject.android.threaded.ThreadItemAdapter;
import org.mockito.Mockito;
/**
@@ -15,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
return forumController;
}
public NestedForumAdapter getAdapter() {
public ThreadItemAdapter<ForumItem> getAdapter() {
return adapter;
}

View File

@@ -3,7 +3,6 @@ package org.briarproject.api.clients;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.Immutable;

View File

@@ -6,6 +6,7 @@ import org.briarproject.api.data.BdfList;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.InvalidMessageException;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
@@ -81,4 +82,8 @@ public interface ClientHelper {
byte[] sign(BdfList toSign, byte[] privateKey)
throws FormatException, GeneralSecurityException;
void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
throws FormatException, GeneralSecurityException;
}

View File

@@ -5,7 +5,6 @@ import org.briarproject.api.identity.Author;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.Immutable;
@@ -14,16 +13,16 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault
public class GroupMessage extends BaseMessage {
private final Author author;
private final Author member;
public GroupMessage(Message message, @Nullable MessageId parent,
Author author) {
Author member) {
super(message, parent);
this.author = author;
this.member = member;
}
public Author getAuthor() {
return author;
public Author getMember() {
return member;
}
}

View File

@@ -1,20 +1,58 @@
package org.briarproject.api.privategroup;
import org.briarproject.api.FormatException;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import java.security.GeneralSecurityException;
import org.jetbrains.annotations.Nullable;
public interface GroupMessageFactory {
@NotNull
/**
* Creates a new member announcement that contains the joiner's identity
* and is signed by the creator.
* <p>
* When a new member accepts an invitation to the group,
* the creator sends this new member announcement to the group.
*
* @param groupId The ID of the group the new member joined
* @param timestamp The current timestamp
* @param creator The creator of the group with {@param groupId}
* @param member The new member that has just accepted an invitation
*/
@CryptoExecutor
GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
LocalAuthor creator, Author member);
/**
* Creates a join announcement message
* that depends on a previous new member announcement.
*
* @param groupId The ID of the Group that is being joined
* @param timestamp Must be equal to the timestamp of the new member message
* @param member Our own LocalAuthor
* @param newMemberId The MessageId of the new member message
*/
@CryptoExecutor
GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor member, MessageId newMemberId);
/**
* Creates a group message
*
* @param groupId The ID of the Group that is posted in
* @param timestamp Must be greater than the timestamps of the parentId
* post, if any, and the member's previous message
* @param parentId The ID of the message that is replied to
* @param author The author of the group message
* @param body The content of the group message
* @param previousMsgId The ID of the author's previous message
* in this group
*/
@CryptoExecutor
GroupMessage createGroupMessage(GroupId groupId, long timestamp,
MessageId parent, LocalAuthor author, String body)
throws FormatException, GeneralSecurityException;
@Nullable MessageId parentId, LocalAuthor author, String body,
MessageId previousMsgId);
}

View File

@@ -3,19 +3,23 @@ package org.briarproject.api.privategroup;
import org.briarproject.api.clients.PostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class GroupMessageHeader extends PostHeader {
private final GroupId groupId;
public GroupMessageHeader(@NotNull GroupId groupId, @NotNull MessageId id,
public GroupMessageHeader(GroupId groupId, MessageId id,
@Nullable MessageId parentId, long timestamp,
@NotNull Author author, @NotNull Status authorStatus,
boolean read) {
Author author, Status authorStatus, boolean read) {
super(id, parentId, timestamp, author, authorStatus, read);
this.groupId = groupId;
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.api.privategroup;
import org.briarproject.api.identity.Author;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class JoinMessageHeader extends GroupMessageHeader {
public JoinMessageHeader(GroupId groupId, MessageId id,
@Nullable MessageId parentId, long timestamp, Author author,
Author.Status authorStatus, boolean read) {
super(groupId, id, parentId, timestamp, author, authorStatus, read);
}
}

View File

@@ -0,0 +1,22 @@
package org.briarproject.api.privategroup;
public enum MessageType {
NEW_MEMBER(0),
JOIN(1),
POST(2);
int value;
MessageType(int value) {
this.value = value;
}
public static MessageType valueOf(int value) {
for (MessageType m : values()) if (m.value == value) return m;
throw new IllegalArgumentException();
}
public int getInt() {
return value;
}
}

View File

@@ -3,57 +3,59 @@ package org.briarproject.api.privategroup;
import org.briarproject.api.clients.MessageTracker;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
public interface PrivateGroupManager extends MessageTracker {
/** Returns the unique ID of the private group client. */
@NotNull
ClientId getClientId();
/** Adds a new private group. */
GroupId addPrivateGroup(String name) throws DbException;
/**
* Adds a new private group and joins it.
*
* @param group The private group to add
* @param newMemberMsg The creator's message announcing herself as
* first new member
* @param joinMsg The creator's own join message
*/
void addPrivateGroup(PrivateGroup group, GroupMessage newMemberMsg,
GroupMessage joinMsg) throws DbException;
/** Removes a dissolved private group. */
void removePrivateGroup(GroupId g) throws DbException;
/** Creates a local group message. */
GroupMessage createLocalMessage(GroupId groupId, String body,
long timestamp, @Nullable MessageId parentId, LocalAuthor author);
/** Gets the MessageId of your previous message sent to the group */
MessageId getPreviousMsgId(GroupId g) throws DbException;
/** Returns the timestamp of the message with the given ID */
// TODO change to getPreviousMessageHeader()
long getMessageTimestamp(MessageId id) throws DbException;
/** Stores (and sends) a local group message. */
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
/** Returns the private group with the given ID. */
@NotNull
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
/**
* Returns the private group with the given ID within the given transaction.
*/
@NotNull
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
/** Returns all private groups the user is a member of. */
@NotNull
Collection<PrivateGroup> getPrivateGroups() throws DbException;
/** Returns true if the private group has been dissolved. */
boolean isDissolved(GroupId g) throws DbException;
/** Returns the body of the group message with the given ID. */
@NotNull
String getMessageBody(MessageId m) throws DbException;
/** Returns the headers of all group messages in the given group. */
@NotNull
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
}

View File

@@ -6,10 +6,6 @@ import org.briarproject.api.blogs.BlogFactory;
import org.briarproject.api.blogs.MessageType;
import org.briarproject.api.clients.BdfMessageContext;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
import org.briarproject.api.data.BdfList;
@@ -48,18 +44,15 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
class BlogPostValidator extends BdfMessageValidator {
private final CryptoComponent crypto;
private final GroupFactory groupFactory;
private final MessageFactory messageFactory;
private final BlogFactory blogFactory;
BlogPostValidator(CryptoComponent crypto, GroupFactory groupFactory,
MessageFactory messageFactory, BlogFactory blogFactory,
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock) {
BlogPostValidator(GroupFactory groupFactory, MessageFactory messageFactory,
BlogFactory blogFactory, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
super(clientHelper, metadataEncoder, clock);
this.crypto = crypto;
this.groupFactory = groupFactory;
this.messageFactory = messageFactory;
this.blogFactory = blogFactory;
@@ -109,7 +102,11 @@ class BlogPostValidator extends BdfMessageValidator {
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), postBody);
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
Author a = b.getAuthor();
verifySignature(sig, a.getPublicKey(), signed);
try {
clientHelper.verifySignature(sig, a.getPublicKey(), signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
// Return the metadata and dependencies
BdfDictionary meta = new BdfDictionary();
@@ -150,7 +147,11 @@ class BlogPostValidator extends BdfMessageValidator {
currentId);
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
Author a = b.getAuthor();
verifySignature(sig, a.getPublicKey(), signed);
try {
clientHelper.verifySignature(sig, a.getPublicKey(), signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
// Return the metadata and dependencies
BdfDictionary meta = new BdfDictionary();
@@ -267,26 +268,6 @@ class BlogPostValidator extends BdfMessageValidator {
return new BdfMessageContext(meta, dependencies);
}
private void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
throws InvalidMessageException {
try {
// Parse the public key
KeyParser keyParser = crypto.getSignatureKeyParser();
PublicKey key = keyParser.parsePublicKey(publicKey);
// Verify the signature
Signature signature = crypto.getSignature();
signature.initVerify(key);
signature.update(clientHelper.toByteArray(signed));
if (!signature.verify(sig)) {
throw new InvalidMessageException("Invalid signature");
}
} catch (GeneralSecurityException e) {
throw new InvalidMessageException("Invalid public key");
} catch (FormatException e) {
throw new InvalidMessageException(e);
}
}
static BdfDictionary authorToBdfDictionary(Author a) {
return BdfDictionary.of(
new BdfEntry(KEY_AUTHOR_ID, a.getId()),

View File

@@ -5,7 +5,6 @@ import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostFactory;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.identity.IdentityManager;
@@ -64,14 +63,14 @@ public class BlogsModule {
@Provides
@Singleton
BlogPostValidator provideBlogPostValidator(
ValidationManager validationManager, CryptoComponent crypto,
GroupFactory groupFactory, MessageFactory messageFactory,
BlogFactory blogFactory, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
ValidationManager validationManager, GroupFactory groupFactory,
MessageFactory messageFactory, BlogFactory blogFactory,
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock) {
BlogPostValidator validator = new BlogPostValidator(crypto,
groupFactory, messageFactory, blogFactory, clientHelper,
metadataEncoder, clock);
BlogPostValidator validator = new BlogPostValidator(groupFactory,
messageFactory, blogFactory, clientHelper, metadataEncoder,
clock);
validationManager.registerMessageValidator(CLIENT_ID, validator);
return validator;

View File

@@ -5,6 +5,7 @@ import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfList;
@@ -320,4 +321,20 @@ class ClientHelperImpl implements ClientHelper {
signature.update(toByteArray(toSign));
return signature.sign();
}
@Override
public void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
throws FormatException, GeneralSecurityException {
// Parse the public key
KeyParser keyParser = cryptoComponent.getSignatureKeyParser();
PublicKey key = keyParser.parsePublicKey(publicKey);
// Verify the signature
Signature signature = cryptoComponent.getSignature();
signature.initVerify(key);
signature.update(toByteArray(signed));
if (!signature.verify(sig)) {
throw new GeneralSecurityException("Invalid signature");
}
}
}

View File

@@ -54,11 +54,11 @@ public class ForumModule {
@Provides
@Singleton
ForumPostValidator provideForumPostValidator(
ValidationManager validationManager, CryptoComponent crypto,
AuthorFactory authorFactory, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
ForumPostValidator validator = new ForumPostValidator(crypto,
authorFactory, clientHelper, metadataEncoder, clock);
ValidationManager validationManager, AuthorFactory authorFactory,
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock) {
ForumPostValidator validator = new ForumPostValidator(authorFactory,
clientHelper, metadataEncoder, clock);
validationManager.registerMessageValidator(
ForumManagerImpl.CLIENT_ID, validator);
return validator;

View File

@@ -4,10 +4,6 @@ import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
import org.briarproject.api.clients.BdfMessageContext;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataEncoder;
@@ -32,14 +28,11 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
class ForumPostValidator extends BdfMessageValidator {
private final CryptoComponent crypto;
private final AuthorFactory authorFactory;
ForumPostValidator(CryptoComponent crypto, AuthorFactory authorFactory,
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock) {
ForumPostValidator(AuthorFactory authorFactory, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
super(clientHelper, metadataEncoder, clock);
this.crypto = crypto;
this.authorFactory = authorFactory;
}
@@ -81,22 +74,14 @@ class ForumPostValidator extends BdfMessageValidator {
}
// Verify the signature, if any
if (author != null) {
// Serialise the data to be verified
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
authorList, contentType, forumPostBody);
try {
// Parse the public key
KeyParser keyParser = crypto.getSignatureKeyParser();
PublicKey key = keyParser.parsePublicKey(author.getPublicKey());
// Serialise the data to be signed
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
authorList, contentType, forumPostBody);
// Verify the signature
Signature signature = crypto.getSignature();
signature.initVerify(key);
signature.update(clientHelper.toByteArray(signed));
if (!signature.verify(sig)) {
throw new InvalidMessageException("Invalid signature");
}
clientHelper
.verifySignature(sig, author.getPublicKey(), signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException("Invalid public key");
throw new InvalidMessageException(e);
}
}
// Return the metadata and dependencies

View File

@@ -1,8 +1,18 @@
package org.briarproject.privategroup;
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
interface Constants {
// Database keys
String KEY_READ = "read";
String KEY_TYPE = "type";
String KEY_TIMESTAMP = "timestamp";
String KEY_READ = MSG_KEY_READ;
String KEY_PARENT_MSG_ID = "parentMsgId";
String KEY_NEW_MEMBER_MSG_ID = "newMemberMsgId";
String KEY_PREVIOUS_MSG_ID = "previousMsgId";
String KEY_MEMBER_ID = "memberId";
String KEY_MEMBER_NAME = "memberName";
String KEY_MEMBER_PUBLIC_KEY = "memberPublicKey";
}

View File

@@ -3,18 +3,25 @@ package org.briarproject.privategroup;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.privategroup.GroupMessage;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.security.GeneralSecurityException;
import javax.inject.Inject;
import static org.briarproject.api.privategroup.MessageType.JOIN;
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
import static org.briarproject.api.privategroup.MessageType.POST;
@NotNullByDefault
class GroupMessageFactoryImpl implements GroupMessageFactory {
private final ClientHelper clientHelper;
@@ -24,20 +31,82 @@ class GroupMessageFactoryImpl implements GroupMessageFactory {
this.clientHelper = clientHelper;
}
@NotNull
@Override
public GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
LocalAuthor creator, Author member) {
try {
// Generate the signature
int type = NEW_MEMBER.getInt();
BdfList toSign = BdfList.of(groupId, timestamp, type,
member.getName(), member.getPublicKey());
byte[] signature =
clientHelper.sign(toSign, creator.getPrivateKey());
// Compose the message
BdfList body =
BdfList.of(type, member.getName(),
member.getPublicKey(), signature);
Message m = clientHelper.createMessage(groupId, timestamp, body);
return new GroupMessage(m, null, member);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (FormatException e) {
throw new RuntimeException(e);
}
}
@Override
public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor member, MessageId newMemberId) {
try {
// Generate the signature
int type = JOIN.getInt();
BdfList toSign = BdfList.of(groupId, timestamp, type,
member.getName(), member.getPublicKey(), newMemberId);
byte[] signature =
clientHelper.sign(toSign, member.getPrivateKey());
// Compose the message
BdfList body =
BdfList.of(type, member.getName(),
member.getPublicKey(), newMemberId, signature);
Message m = clientHelper.createMessage(groupId, timestamp, body);
return new GroupMessage(m, null, member);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (FormatException e) {
throw new RuntimeException(e);
}
}
@Override
public GroupMessage createGroupMessage(GroupId groupId, long timestamp,
MessageId parent, LocalAuthor author, String body)
throws FormatException, GeneralSecurityException {
@Nullable MessageId parentId, LocalAuthor author, String content,
MessageId previousMsgId) {
try {
// Generate the signature
int type = POST.getInt();
BdfList toSign = BdfList.of(groupId, timestamp, type,
author.getName(), author.getPublicKey(), parentId,
previousMsgId, content);
byte[] signature =
clientHelper.sign(toSign, author.getPrivateKey());
// Generate the signature
byte[] sig = clientHelper.sign(new BdfList(), author.getPrivateKey());
// Compose the message
BdfList body =
BdfList.of(type, author.getName(),
author.getPublicKey(), parentId, previousMsgId,
content, signature);
Message m = clientHelper.createMessage(groupId, timestamp, body);
// Compose the message
Message m =
clientHelper.createMessage(groupId, timestamp, new BdfList());
return new GroupMessage(m, parent, author);
return new GroupMessage(m, parentId, author);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (FormatException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -3,11 +3,14 @@ package org.briarproject.privategroup;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.BdfMessageContext;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.privategroup.MessageType;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.InvalidMessageException;
import org.briarproject.api.sync.Message;
@@ -15,19 +18,38 @@ import org.briarproject.api.sync.MessageId;
import org.briarproject.api.system.Clock;
import org.briarproject.clients.BdfMessageValidator;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.api.privategroup.MessageType.JOIN;
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
import static org.briarproject.api.privategroup.MessageType.POST;
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_READ;
import static org.briarproject.privategroup.Constants.KEY_TIMESTAMP;
import static org.briarproject.privategroup.Constants.KEY_TYPE;
class GroupMessageValidator extends BdfMessageValidator {
private final CryptoComponent crypto;
private final PrivateGroupFactory groupFactory;
private final AuthorFactory authorFactory;
GroupMessageValidator(CryptoComponent crypto, AuthorFactory authorFactory,
GroupMessageValidator(PrivateGroupFactory groupFactory,
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock) {
Clock clock, AuthorFactory authorFactory) {
super(clientHelper, metadataEncoder, clock);
this.crypto = crypto;
this.groupFactory = groupFactory;
this.authorFactory = authorFactory;
}
@@ -35,9 +57,168 @@ class GroupMessageValidator extends BdfMessageValidator {
protected BdfMessageContext validateMessage(Message m, Group g,
BdfList body) throws InvalidMessageException, FormatException {
checkSize(body, 4, 7);
// message type (int)
int type = body.getLong(0).intValue();
body.removeElementAt(0);
// member_name (string)
String memberName = body.getString(0);
checkLength(memberName, 1, MAX_AUTHOR_NAME_LENGTH);
// member_public_key (raw)
byte[] memberPublicKey = body.getRaw(1);
checkLength(memberPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
BdfMessageContext c;
switch (MessageType.valueOf(type)) {
case NEW_MEMBER:
c = validateNewMember(m, g, body, memberName,
memberPublicKey);
addMessageMetadata(c, memberName, memberPublicKey,
m.getTimestamp());
break;
case JOIN:
c = validateJoin(m, g, body, memberName, memberPublicKey);
addMessageMetadata(c, memberName, memberPublicKey,
m.getTimestamp());
break;
case POST:
c = validatePost(m, g, body, memberName, memberPublicKey);
addMessageMetadata(c, memberName, memberPublicKey,
m.getTimestamp());
break;
default:
throw new InvalidMessageException("Unknown Message Type");
}
c.getDictionary().put(KEY_TYPE, type);
return c;
}
private BdfMessageContext validateNewMember(Message m, Group g,
BdfList body, String memberName, byte[] memberPublicKey)
throws InvalidMessageException, FormatException {
// The content is a BDF list with three elements
checkSize(body, 3);
// signature (raw)
// signature with the creator's private key over a list with 4 elements
byte[] signature = body.getRaw(2);
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
// Verify Signature
BdfList signed =
BdfList.of(g.getId(), m.getTimestamp(), NEW_MEMBER.getInt(),
memberName, memberPublicKey);
PrivateGroup group = groupFactory.parsePrivateGroup(g);
byte[] creatorPublicKey = group.getAuthor().getPublicKey();
try {
clientHelper.verifySignature(signature, creatorPublicKey, signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
// Return the metadata and no dependencies
BdfDictionary meta = new BdfDictionary();
Collection<MessageId> dependencies = Collections.emptyList();
return new BdfMessageContext(meta);
}
private BdfMessageContext validateJoin(Message m, Group g, BdfList body,
String memberName, byte[] memberPublicKey)
throws InvalidMessageException, FormatException {
// The content is a BDF list with four elements
checkSize(body, 4);
// new_member_id (raw)
// the identifier of a new member message
// with the same member_name and member_public_key
byte[] newMemberId = body.getRaw(2);
checkLength(newMemberId, MessageId.LENGTH);
// signature (raw)
// a signature with the member's private key over a list with 5 elements
byte[] signature = body.getRaw(3);
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
// Verify Signature
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), JOIN.getInt(),
memberName, memberPublicKey, newMemberId);
try {
clientHelper.verifySignature(signature, memberPublicKey, signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
// The new member message is a dependency
Collection<MessageId> dependencies =
Collections.singleton(new MessageId(newMemberId));
// Return the metadata and dependencies
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_NEW_MEMBER_MSG_ID, newMemberId);
return new BdfMessageContext(meta, dependencies);
}
private BdfMessageContext validatePost(Message m, Group g, BdfList body,
String memberName, byte[] memberPublicKey)
throws InvalidMessageException, FormatException {
// The content is a BDF list with six elements
checkSize(body, 6);
// parent_id (raw or null)
// the identifier of the post to which this is a reply, if any
byte[] parentId = body.getOptionalRaw(2);
checkLength(parentId, MessageId.LENGTH);
// previous_message_id (raw)
// the identifier of the member's previous post or join message
byte[] previousMessageId = body.getRaw(3);
checkLength(previousMessageId, MessageId.LENGTH);
// content (string)
String content = body.getString(4);
checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH);
// signature (raw)
// a signature with the member's private key over a list with 7 elements
byte[] signature = body.getRaw(5);
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
// Verify Signature
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), POST.getInt(),
memberName, memberPublicKey, parentId, previousMessageId,
content);
try {
clientHelper.verifySignature(signature, memberPublicKey, signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
// The parent post, if any,
// and the member's previous message are dependencies
Collection<MessageId> dependencies = new ArrayList<MessageId>();
if (parentId != null) dependencies.add(new MessageId(parentId));
dependencies.add(new MessageId(previousMessageId));
// Return the metadata and dependencies
BdfDictionary meta = new BdfDictionary();
if (parentId != null) meta.put(KEY_PARENT_MSG_ID, parentId);
meta.put(KEY_PREVIOUS_MSG_ID, previousMessageId);
return new BdfMessageContext(meta, dependencies);
}
private void addMessageMetadata(BdfMessageContext c, String authorName,
byte[] pubKey, long time) {
c.getDictionary().put(KEY_TIMESTAMP, time);
c.getDictionary().put(KEY_READ, false);
Author a = authorFactory.createAuthor(authorName, pubKey);
c.getDictionary().put(KEY_MEMBER_ID, a.getId());
c.getDictionary().put(KEY_MEMBER_NAME, authorName);
c.getDictionary().put(KEY_MEMBER_PUBLIC_KEY, pubKey);
}
}

View File

@@ -3,16 +3,20 @@ package org.briarproject.privategroup;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataParser;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.privategroup.GroupMessage;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.privategroup.JoinMessageHeader;
import org.briarproject.api.privategroup.MessageType;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
@@ -21,21 +25,34 @@ import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.system.Clock;
import org.briarproject.clients.BdfIncomingMessageHook;
import org.briarproject.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.api.identity.Author.Status.OURSELVES;
import static org.briarproject.api.privategroup.MessageType.JOIN;
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
import static org.briarproject.api.privategroup.MessageType.POST;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_READ;
import static org.briarproject.privategroup.Constants.KEY_TIMESTAMP;
import static org.briarproject.privategroup.Constants.KEY_TYPE;
public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
PrivateGroupManager {
@@ -46,62 +63,102 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
StringUtils.fromHexString("5072697661746547726f75704d616e61"
+ "67657220627920546f727374656e2047"));
private final IdentityManager identityManager;
private final PrivateGroupFactory privateGroupFactory;
private final GroupMessageFactory groupMessageFactory;
private final Clock clock;
private final IdentityManager identityManager;
@Inject
PrivateGroupManagerImpl(ClientHelper clientHelper,
MetadataParser metadataParser, DatabaseComponent db,
IdentityManager identityManager,
PrivateGroupFactory privateGroupFactory,
GroupMessageFactory groupMessageFactory, Clock clock) {
IdentityManager identityManager) {
super(db, clientHelper, metadataParser);
this.identityManager = identityManager;
this.privateGroupFactory = privateGroupFactory;
this.groupMessageFactory = groupMessageFactory;
this.clock = clock;
this.identityManager = identityManager;
}
@NotNull
@Override
public ClientId getClientId() {
return CLIENT_ID;
}
@Override
public GroupId addPrivateGroup(String name) throws DbException {
PrivateGroup group;
public void addPrivateGroup(PrivateGroup group,
GroupMessage newMemberMsg, GroupMessage joinMsg)
throws DbException {
Transaction txn = db.startTransaction(false);
try {
LocalAuthor a = identityManager.getLocalAuthor(txn);
group = privateGroupFactory.createPrivateGroup(name, a);
db.addGroup(txn, group.getGroup());
announceNewMember(txn, newMemberMsg);
joinPrivateGroup(txn, joinMsg);
txn.setComplete();
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
return group.getId();
}
private void announceNewMember(Transaction txn, GroupMessage m)
throws DbException, FormatException {
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_TYPE, NEW_MEMBER.getInt());
addMessageMetadata(meta, m, true);
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
}
private void joinPrivateGroup(Transaction txn, GroupMessage m)
throws DbException, FormatException {
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_TYPE, JOIN.getInt());
addMessageMetadata(meta, m, true);
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
trackOutgoingMessage(txn, m.getMessage());
setPreviousMsgId(txn, m.getMessage().getGroupId(),
m.getMessage().getId());
}
@Override
public void removePrivateGroup(GroupId g) throws DbException {
// TODO
}
@Override
public GroupMessage createLocalMessage(GroupId groupId, String body,
long timestamp, @Nullable MessageId parentId, LocalAuthor author) {
public MessageId getPreviousMsgId(GroupId g) throws DbException {
MessageId previousMsgId;
Transaction txn = db.startTransaction(true);
try {
return groupMessageFactory
.createGroupMessage(groupId, timestamp, parentId, author,
body);
previousMsgId = getPreviousMsgId(txn, g);
txn.setComplete();
} catch (FormatException e) {
throw new RuntimeException(e);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
return previousMsgId;
}
private MessageId getPreviousMsgId(Transaction txn, GroupId g)
throws DbException, FormatException {
BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g);
byte[] previousMsgIdBytes = d.getRaw(KEY_PREVIOUS_MSG_ID);
return new MessageId(previousMsgIdBytes);
}
private void setPreviousMsgId(Transaction txn, GroupId g,
MessageId previousMsgId) throws DbException, FormatException {
BdfDictionary d = BdfDictionary
.of(new BdfEntry(KEY_PREVIOUS_MSG_ID, previousMsgId));
clientHelper.mergeGroupMetadata(txn, g, d);
}
@Override
public long getMessageTimestamp(MessageId id) throws DbException {
try {
BdfDictionary d = clientHelper.getMessageMetadataAsDictionary(id);
return d.getLong(KEY_TIMESTAMP);
} catch (FormatException e) {
throw new DbException(e);
}
}
@@ -111,7 +168,12 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
Transaction txn = db.startTransaction(false);
try {
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_TYPE, POST.getInt());
if (m.getParent() != null) meta.put(KEY_PARENT_MSG_ID, m.getParent());
addMessageMetadata(meta, m, true);
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
setPreviousMsgId(txn, m.getMessage().getGroupId(),
m.getMessage().getId());
trackOutgoingMessage(txn, m.getMessage());
txn.setComplete();
} catch (FormatException e) {
@@ -121,10 +183,18 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
}
return new GroupMessageHeader(m.getMessage().getGroupId(),
m.getMessage().getId(), m.getParent(),
m.getMessage().getTimestamp(), m.getAuthor(), OURSELVES, true);
m.getMessage().getTimestamp(), m.getMember(), OURSELVES, true);
}
private void addMessageMetadata(BdfDictionary meta, GroupMessage m,
boolean read) {
meta.put(KEY_TIMESTAMP, m.getMessage().getTimestamp());
meta.put(KEY_READ, read);
meta.put(KEY_MEMBER_ID, m.getMember().getId());
meta.put(KEY_MEMBER_NAME, m.getMember().getName());
meta.put(KEY_MEMBER_PUBLIC_KEY, m.getMember().getPublicKey());
}
@NotNull
@Override
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
PrivateGroup privateGroup;
@@ -138,7 +208,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
return privateGroup;
}
@NotNull
@Override
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
throws DbException {
@@ -150,7 +219,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
}
}
@NotNull
@Override
public Collection<PrivateGroup> getPrivateGroups() throws DbException {
Collection<Group> groups;
@@ -178,27 +246,179 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
return false;
}
@NotNull
@Override
public String getMessageBody(MessageId m) throws DbException {
return "empty";
try {
// type(0), member_name(1), member_public_key(2), parent_id(3),
// previous_message_id(4), content(5), signature(6)
return clientHelper.getMessageAsList(m).getString(5);
} catch (FormatException e) {
throw new DbException(e);
}
}
@NotNull
@Override
public Collection<GroupMessageHeader> getHeaders(GroupId g)
throws DbException {
Collection<GroupMessageHeader> headers =
new ArrayList<GroupMessageHeader>();
Transaction txn = db.startTransaction(true);
try {
Map<MessageId, BdfDictionary> metadata =
clientHelper.getMessageMetadataAsDictionary(txn, g);
// get all authors we need to get the status for
Set<AuthorId> authors = new HashSet<AuthorId>();
for (BdfDictionary meta : metadata.values()) {
if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
continue;
byte[] idBytes = meta.getRaw(KEY_MEMBER_ID);
authors.add(new AuthorId(idBytes));
}
// get statuses for all authors
Map<AuthorId, Status> statuses = new HashMap<AuthorId, Status>();
for (AuthorId id : authors) {
statuses.put(id, identityManager.getAuthorStatus(txn, id));
}
// Parse the metadata
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
BdfDictionary meta = entry.getValue();
if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
continue;
headers.add(getGroupMessageHeader(txn, g, entry.getKey(), meta,
statuses));
}
txn.setComplete();
return headers;
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
}
return Collections.emptyList();
private GroupMessageHeader getGroupMessageHeader(Transaction txn, GroupId g,
MessageId id, BdfDictionary meta, Map<AuthorId, Status> statuses)
throws DbException, FormatException {
MessageId parentId = null;
if (meta.containsKey(KEY_PARENT_MSG_ID)) {
parentId = new MessageId(meta.getRaw(KEY_PARENT_MSG_ID));
}
long timestamp = meta.getLong(KEY_TIMESTAMP);
AuthorId authorId = new AuthorId(meta.getRaw(KEY_MEMBER_ID));
String name = meta.getString(KEY_MEMBER_NAME);
byte[] publicKey = meta.getRaw(KEY_MEMBER_PUBLIC_KEY);
Author author = new Author(authorId, name, publicKey);
Status status;
if (statuses.containsKey(authorId)) {
status = statuses.get(authorId);
} else {
status = identityManager.getAuthorStatus(txn, author.getId());
}
boolean read = meta.getBoolean(KEY_READ);
if (meta.getLong(KEY_TYPE) == JOIN.getInt()) {
return new JoinMessageHeader(g, id, parentId, timestamp, author,
status, read);
}
return new GroupMessageHeader(g, id, parentId, timestamp, author,
status, read);
}
@Override
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
BdfDictionary meta) throws DbException, FormatException {
trackIncomingMessage(txn, m);
return true;
long timestamp = meta.getLong(KEY_TIMESTAMP);
MessageType type =
MessageType.valueOf(meta.getLong(KEY_TYPE).intValue());
switch (type) {
case NEW_MEMBER:
// don't track incoming message, because it won't show in the UI
return true;
case JOIN:
// new_member_id must be the identifier of a NEW_MEMBER message
byte[] newMemberIdBytes =
meta.getOptionalRaw(KEY_NEW_MEMBER_MSG_ID);
MessageId newMemberId = new MessageId(newMemberIdBytes);
BdfDictionary newMemberMeta = clientHelper
.getMessageMetadataAsDictionary(txn, newMemberId);
MessageType newMemberType = MessageType
.valueOf(newMemberMeta.getLong(KEY_TYPE).intValue());
if (newMemberType != NEW_MEMBER) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// timestamp must be equal to timestamp of NEW_MEMBER message
if (timestamp != newMemberMeta.getLong(KEY_TIMESTAMP)) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// NEW_MEMBER must have same member_name and member_public_key
if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
newMemberMeta.getRaw(KEY_MEMBER_ID))) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// TODO add to member list
trackIncomingMessage(txn, m);
return true;
case POST:
// timestamp must be greater than the timestamps of parent post
byte[] parentIdBytes = meta.getOptionalRaw(KEY_PARENT_MSG_ID);
if (parentIdBytes != null) {
MessageId parentId = new MessageId(parentIdBytes);
BdfDictionary parentMeta = clientHelper
.getMessageMetadataAsDictionary(txn, parentId);
if (timestamp <= parentMeta.getLong(KEY_TIMESTAMP)) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
MessageType parentType = MessageType
.valueOf(parentMeta.getLong(KEY_TYPE).intValue());
if (parentType != POST) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
}
// and the member's previous message
byte[] previousMsgIdBytes = meta.getRaw(KEY_PREVIOUS_MSG_ID);
MessageId previousMsgId = new MessageId(previousMsgIdBytes);
BdfDictionary previousMeta = clientHelper
.getMessageMetadataAsDictionary(txn, previousMsgId);
if (timestamp <= previousMeta.getLong(KEY_TIMESTAMP)) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// previous message must be from same member
if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
previousMeta.getRaw(KEY_MEMBER_ID))) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// previous message must be a POST or JOIN
MessageType previousType = MessageType
.valueOf(previousMeta.getLong(KEY_TYPE).intValue());
if (previousType != JOIN && previousType != POST) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
trackIncomingMessage(txn, m);
return true;
default:
// the validator should only let valid types pass
throw new RuntimeException("Unknown MessageType");
}
}
}

View File

@@ -2,7 +2,6 @@ package org.briarproject.privategroup;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.lifecycle.LifecycleManager;
@@ -59,13 +58,17 @@ public class PrivateGroupModule {
@Provides
@Singleton
GroupMessageValidator provideGroupMessageValidator(
ValidationManager validationManager, CryptoComponent crypto,
AuthorFactory authorFactory, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock) {
GroupMessageValidator validator = new GroupMessageValidator(crypto,
authorFactory, clientHelper, metadataEncoder, clock);
PrivateGroupFactory groupFactory,
ValidationManager validationManager, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock,
AuthorFactory authorFactory) {
GroupMessageValidator validator = new GroupMessageValidator(
groupFactory, clientHelper, metadataEncoder, clock,
authorFactory);
validationManager.registerMessageValidator(
PrivateGroupManagerImpl.CLIENT_ID, validator);
return validator;
}

View File

@@ -7,9 +7,6 @@ import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogFactory;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
import org.briarproject.api.data.BdfList;
@@ -20,7 +17,6 @@ import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.InvalidMessageException;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageFactory;
import org.briarproject.api.sync.MessageId;
@@ -37,9 +33,9 @@ import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR;
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID;
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME;
import static org.briarproject.api.blogs.BlogConstants.KEY_COMMENT;
import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_MSG_ID;
import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_PARENT_MSG_ID;
import static org.briarproject.api.blogs.BlogConstants.KEY_PARENT_MSG_ID;
import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_MSG_ID;
import static org.briarproject.api.blogs.BlogConstants.KEY_PUBLIC_KEY;
import static org.briarproject.api.blogs.BlogConstants.KEY_READ;
import static org.briarproject.api.blogs.MessageType.COMMENT;
@@ -94,9 +90,8 @@ public class BlogPostValidatorTest extends BriarTestCase {
message = new Message(messageId, group.getId(), timestamp, raw);
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
validator = new BlogPostValidator(cryptoComponent, groupFactory,
messageFactory, blogFactory, clientHelper, metadataEncoder,
clock);
validator = new BlogPostValidator(groupFactory, messageFactory,
blogFactory, clientHelper, metadataEncoder, clock);
context.assertIsSatisfied();
}
@@ -108,7 +103,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), body);
expectCrypto(signed, sigBytes, true);
expectCrypto(signed, sigBytes);
final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary();
@@ -135,18 +130,6 @@ public class BlogPostValidatorTest extends BriarTestCase {
validator.validateMessage(message, group, m).getDictionary();
}
@Test(expected = InvalidMessageException.class)
public void testValidateBlogPostWithBadSignature()
throws IOException, GeneralSecurityException {
final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m = BdfList.of(POST.getInt(), body, sigBytes);
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), body);
expectCrypto(signed, sigBytes, false);
validator.validateMessage(message, group, m).getDictionary();
}
@Test
public void testValidateProperBlogComment()
throws IOException, GeneralSecurityException {
@@ -162,7 +145,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), comment,
pOriginalId, currentId);
expectCrypto(signed, sigBytes, true);
expectCrypto(signed, sigBytes);
final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary();
@@ -189,7 +172,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), null,
originalId, currentId);
expectCrypto(signed, sigBytes, true);
expectCrypto(signed, sigBytes);
final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary();
@@ -208,7 +191,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), body);
expectCrypto(signed, sigBytes, true);
expectCrypto(signed, sigBytes);
final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
final byte[] originalBody = TestUtils.getRandomBytes(42);
@@ -247,7 +230,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
comment, originalId, oldId);
expectCrypto(signed, sigBytes, true);
expectCrypto(signed, sigBytes);
final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
originalId, oldId, sigBytes);
@@ -275,27 +258,13 @@ public class BlogPostValidatorTest extends BriarTestCase {
context.assertIsSatisfied();
}
private void expectCrypto(final BdfList signed, final byte[] sig,
final boolean pass) throws IOException, GeneralSecurityException {
final Signature signature = context.mock(Signature.class);
final KeyParser keyParser = context.mock(KeyParser.class);
final PublicKey publicKey = context.mock(PublicKey.class);
private void expectCrypto(final BdfList signed, final byte[] sig)
throws IOException, GeneralSecurityException {
context.checking(new Expectations() {{
oneOf(blogFactory).parseBlog(group, "");
will(returnValue(blog));
oneOf(cryptoComponent).getSignatureKeyParser();
will(returnValue(keyParser));
oneOf(keyParser).parsePublicKey(blog.getAuthor().getPublicKey());
will(returnValue(publicKey));
oneOf(cryptoComponent).getSignature();
will(returnValue(signature));
oneOf(signature).initVerify(publicKey);
oneOf(clientHelper).toByteArray(signed);
will(returnValue(sig));
oneOf(signature).update(sig);
oneOf(signature).verify(sig);
will(returnValue(pass));
oneOf(clientHelper)
.verifySignature(sig, author.getPublicKey(), signed);
}});
}