diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java index 60a1ba3c3fa823f1cae110e01c28179bd7894d87..0339413b19383c76489dc9ebc69143b695f5de1c 100644 --- a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java @@ -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; diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..18a6d5eb438981b0dd3a3731e787a37c106fa5e1 --- /dev/null +++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java @@ -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()); + } + +} diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..68191f4f87b7099a9723749fb5d12184a050ab6a --- /dev/null +++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java @@ -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(); + +} diff --git a/briar-android/res/layout/list_item_forum_post.xml b/briar-android/res/layout/list_item_thread.xml similarity index 98% rename from briar-android/res/layout/list_item_forum_post.xml rename to briar-android/res/layout/list_item_thread.xml index 2fe00672178eab9dafc8e92dfd062626fec77260..f788c0ce8e6aff841c4d3f4dcae07e423576966c 100644 --- a/briar-android/res/layout/list_item_forum_post.xml +++ b/briar-android/res/layout/list_item_thread.xml @@ -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" diff --git a/briar-android/res/layout/list_item_thread_notice.xml b/briar-android/res/layout/list_item_thread_notice.xml new file mode 100644 index 0000000000000000000000000000000000000000..2beecd1f2aabf4c2a2b209974e6d383268e3d8b1 --- /dev/null +++ b/briar-android/res/layout/list_item_thread_notice.xml @@ -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> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 878b281e48a5de9ec154e3b7b0ca69a2bf8467fe..132cea768dd5f12ffc8b3c4265d99fb5354eadeb 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -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> diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java index 174a5c94668a193735716965d889c99644626b21..6550d1752b6b05fbd484da2f97d506bfd332fb91 100644 --- a/briar-android/src/org/briarproject/android/ActivityModule.java +++ b/briar-android/src/org/briarproject/android/ActivityModule.java @@ -120,6 +120,7 @@ public class ActivityModule { @Provides protected GroupController provideGroupController( GroupControllerImpl groupController) { + activity.addLifecycleController(groupController); return groupController; } diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index 6081f1da9b5172bc1c1a6d2e7a74c47f37b6ad27..a436f9430fc9dc9d0e6b3582b8ed87f928160220 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -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(); diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index 307a7c5e158407d4a0cd173974607da181716181..36d552473a2684fbd3974a159c11d43d0a5c5ede 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -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 diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java index a0b89b0ed4aa4d9363bf20f4fb0487a3de5fc313..25d8328464377f8bff53ea5b33e018265db19538 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java +++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java @@ -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 diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java deleted file mode 100644 index 08d68b961301b4883a01e5c1f36ac6eb5ea83795..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java +++ /dev/null @@ -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); - } - -} diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java deleted file mode 100644 index b73558ff5d668c1adf37f0e8e69a9f971411967c..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java +++ /dev/null @@ -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); - } - -} diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java index 71a4865b516bf1e14e8b5d1ef9c6317179092c2a..d17ed8d66374e1080c2761bce0a9f382a98c91ca 100644 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java @@ -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; diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java index 8a6644efddaebff7b71a226c7631604e28b6f284..abbf10d423508e41b7459f74f389e48f9ea67b2d 100644 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java @@ -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); } diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java index 19ee14adce62158808c7f795e9eb505ec889cd2f..c042a182965f3dbe067a3a5a343898c1f13cb0f1 100644 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java @@ -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); } } diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java index 7bde4a8bbcf183f06f275d7fbb0c49395259aea1..9deb0424e574fc582b56dc687246c3f74d3438fb 100644 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java @@ -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; + } + } diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java deleted file mode 100644 index 11825b8056e6ee3002338956a6599c2a4b949b21..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java +++ /dev/null @@ -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); - } - -} diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java new file mode 100644 index 0000000000000000000000000000000000000000..44c732ffd48970cc59a4abd2a92e4bebac232219 --- /dev/null +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItemViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItemViewHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..972a6149eaa0d3093156c1df28bbe2c43fa77954 --- /dev/null +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItemViewHolder.java @@ -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)); + } + +} diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java index a35c2ac20435a7486ecbc1407989c3216d1b42e1..db661c43da5863f4b5a5805e7434ab88e2775e8b 100644 --- a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java +++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java @@ -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); diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java b/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java similarity index 56% rename from briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java rename to briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java index 0b5a5ddc5b74cc34bee5bb6d63b2534ce2aa0227..af8e800b33ed2d308198ffc58a914279a10abea2 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java +++ b/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java @@ -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(); } diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java index a5a22075995246e5859671bc7b64995324bb75ea..e4c055e2b39a31c228ff72c77964d76dd48a5ce2 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java +++ b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java @@ -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; } + } diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java index c8ab7f2ba0bf173f8688939167c40db894683663..7c3ee9079128f1d1424ab570e78f1da3c3b126e4 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java +++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java @@ -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); diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java index a8eefd250f474069e20a40fb458af1013c43976f..eff17e175e13264ea28ab2381ae422d87e2e0aef 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java +++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java @@ -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(""); diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java index a7731d8d48ed2046f39c962e8781b86c4c9c396d..f2e7570a820d89f66cf7e3452d74b3a1a4256514 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java +++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java @@ -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); diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java index 80a5b44ea2ab4a968dc897dcd5d6c6f517c7b84a..6ea72c892357f669bc1fcd8d322ddca3b4b787cc 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java +++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java @@ -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 diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadPostViewHolder.java b/briar-android/src/org/briarproject/android/threaded/ThreadPostViewHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..bffd1d6eaeb5a73f6b30ebedc617a37ded04b06c --- /dev/null +++ b/briar-android/src/org/briarproject/android/threaded/ThreadPostViewHolder.java @@ -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); + } + }); + } + +} diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java index 991f59f0f26dca492ff6b7204871430f0c0d4928..cedacc77d08850c7dc891bd4647f215150bb9f5f 100644 --- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java +++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java @@ -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()); diff --git a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java index 806fee299bf73a5bb23a034dfd8541765b1b29d3..a77a88cc156c4771d0da6689eca875539f624037 100644 --- a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java +++ b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java @@ -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; } diff --git a/briar-api/src/org/briarproject/api/clients/BaseMessage.java b/briar-api/src/org/briarproject/api/clients/BaseMessage.java index fbfb56337789d1e2cb3dbee512fc5e170b04b84e..ee3c0319f484e0d58889681ed1acd20f30b24d4c 100644 --- a/briar-api/src/org/briarproject/api/clients/BaseMessage.java +++ b/briar-api/src/org/briarproject/api/clients/BaseMessage.java @@ -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; diff --git a/briar-api/src/org/briarproject/api/clients/ClientHelper.java b/briar-api/src/org/briarproject/api/clients/ClientHelper.java index 5aeffaa21ec026a5708cb281bcdf721206054a89..b24c9d2b715456a4137bef4246209bdc5853a741 100644 --- a/briar-api/src/org/briarproject/api/clients/ClientHelper.java +++ b/briar-api/src/org/briarproject/api/clients/ClientHelper.java @@ -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; + } diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java index 06f277460e7828d4231bbefc58b4a127e9b7be2c..4de6bbbb0a0430fba36ef09ddf1c2dd0ac610958 100644 --- a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java +++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java @@ -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; } } diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java index 76f46314e96b752f3aba60f3eb69d3f1ee0d3281..26e7ae9c7b73f533b5136fc05a709d28347e171d 100644 --- a/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java +++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java @@ -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); } diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java index 21043297bb338e51de4b76a73800dd970207280a..9789a59e0ac2e31ddd1e46a7ad7bb69c1fe78bbe 100644 --- a/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java +++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java @@ -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; } diff --git a/briar-api/src/org/briarproject/api/privategroup/JoinMessageHeader.java b/briar-api/src/org/briarproject/api/privategroup/JoinMessageHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..ef8c3b337d72124fd5d42b075f5e8edea894a53f --- /dev/null +++ b/briar-api/src/org/briarproject/api/privategroup/JoinMessageHeader.java @@ -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); + } + +} diff --git a/briar-api/src/org/briarproject/api/privategroup/MessageType.java b/briar-api/src/org/briarproject/api/privategroup/MessageType.java new file mode 100644 index 0000000000000000000000000000000000000000..7cffb0df312973ac3b4955547037782aa7a294fe --- /dev/null +++ b/briar-api/src/org/briarproject/api/privategroup/MessageType.java @@ -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; + } +} diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java index f2253c99a7c0c1999c1910146602e586ba4cf153..49bee81f8788f1edacda917d4d0244544fe001e8 100644 --- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java +++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java @@ -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; } diff --git a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java index 29908c6d3d8230368e9fe4abc508e2eeefe12f8e..cd2347bc65ea79d8b3110ff28b556fab4a169eb4 100644 --- a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java +++ b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java @@ -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()), diff --git a/briar-core/src/org/briarproject/blogs/BlogsModule.java b/briar-core/src/org/briarproject/blogs/BlogsModule.java index 91cc68c7c5ff2d5052367aff744d16ace49a2293..e8927d5afc374a7c4b29d8593b9214810f922150 100644 --- a/briar-core/src/org/briarproject/blogs/BlogsModule.java +++ b/briar-core/src/org/briarproject/blogs/BlogsModule.java @@ -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; diff --git a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java index d7a983b0ae43b2fcc9e01832b1ed47efb4572148..9c0354419a0250f0ae85c122e7da4519d99c1f48 100644 --- a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java +++ b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java @@ -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"); + } + } + } diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java index 40bfd01381fe8b1972fb253ec389c7b18107e006..eeafb54dfc46b9ab7b12cff3ea93fef5e7944b1e 100644 --- a/briar-core/src/org/briarproject/forum/ForumModule.java +++ b/briar-core/src/org/briarproject/forum/ForumModule.java @@ -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; diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java index 258ac9bb5f23e6305864d46d6076284b93f86e35..ce4bddb4364212326571fdce95d9dee020749dfc 100644 --- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java +++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java @@ -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 diff --git a/briar-core/src/org/briarproject/privategroup/Constants.java b/briar-core/src/org/briarproject/privategroup/Constants.java index 7a1a4495bf104b7c083cf96bbb5965f20024c7c1..12219f78d789193398edea716259c41cba7f7847 100644 --- a/briar-core/src/org/briarproject/privategroup/Constants.java +++ b/briar-core/src/org/briarproject/privategroup/Constants.java @@ -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"; } diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java b/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java index 271f7712c02c3b24373bddbb715a77e36f490400..228b6425448958ae1ebaee41e372160b8092a078 100644 --- a/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java +++ b/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java @@ -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 createGroupMessage(GroupId groupId, long timestamp, - MessageId parent, LocalAuthor author, String body) - throws FormatException, GeneralSecurityException { + 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); + } + } - // Generate the signature - byte[] sig = clientHelper.sign(new BdfList(), author.getPrivateKey()); + @Override + public GroupMessage createGroupMessage(GroupId groupId, long timestamp, + @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()); - // Compose the message - Message m = - clientHelper.createMessage(groupId, timestamp, new BdfList()); + // Compose the message + BdfList body = + BdfList.of(type, author.getName(), + author.getPublicKey(), parentId, previousMsgId, + content, signature); + Message m = clientHelper.createMessage(groupId, timestamp, body); - 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); + } } } diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java index 75a8913c07a677efdeb4d5ef01cb85487bb473bf..25bf14730cc4b988b91728de14f9ef10fa22f2f7 100644 --- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java +++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java @@ -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(); + 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(); - Collection<MessageId> dependencies = Collections.emptyList(); + 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); + } + } diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java index 05c2d4021c4c766a2cdbcb89baea7e792a594aeb..769e065b87a200cd8e80b2cbcc64bb4c42fe5437 100644 --- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java +++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java @@ -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 MessageId getPreviousMsgId(GroupId g) throws DbException { + MessageId previousMsgId; + Transaction txn = db.startTransaction(true); + try { + previousMsgId = getPreviousMsgId(txn, g); + txn.setComplete(); + } catch (FormatException 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 GroupMessage createLocalMessage(GroupId groupId, String body, - long timestamp, @Nullable MessageId parentId, LocalAuthor author) { + public long getMessageTimestamp(MessageId id) throws DbException { try { - return groupMessageFactory - .createGroupMessage(groupId, timestamp, parentId, author, - body); + BdfDictionary d = clientHelper.getMessageMetadataAsDictionary(id); + return d.getLong(KEY_TIMESTAMP); } catch (FormatException e) { - throw new RuntimeException(e); - } catch (GeneralSecurityException e) { - throw new RuntimeException(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); + } + } + + 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); - return Collections.emptyList(); + 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"); + } } } diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java index 570f697a629d952be8790dd5b27e04e1a21a2a90..49c0714c5cb0987c2fd5c602578ec498e42efdf0 100644 --- a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java +++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java @@ -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; } diff --git a/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java b/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java index bdbcc2398ce40f81da6e8b4fafc5509fb5832f37..afb1b90b9f5a78e5d3144d8acb37fdc6bd872319 100644 --- a/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java +++ b/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java @@ -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); }}); }