diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java index e516134467886402082cb7c16d0f16d1dcddb422..bfbdf5c8c79d66a02f8695c2d40c685e59c1555b 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java @@ -49,6 +49,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; @@ -65,15 +66,15 @@ import static org.junit.Assert.assertTrue; public class ForumSharingIntegrationTest extends BriarTestCase { - LifecycleManager lifecycleManager0, lifecycleManager1; - SyncSessionFactory sync0, sync1; - ForumManager forumManager0, forumManager1; - ContactManager contactManager0, contactManager1; - ContactId contactId0, contactId1; - IdentityManager identityManager0, identityManager1; - LocalAuthor author0, author1; + LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2; + SyncSessionFactory sync0, sync1, sync2; + ForumManager forumManager0, forumManager1, forumManager2; + ContactManager contactManager0, contactManager1, contactManager2; + ContactId contactId0, contactId2, contactId1, contactId21; + IdentityManager identityManager0, identityManager1, identityManager2; + LocalAuthor author0, author1, author2; Forum forum0; - SharerListener listener0; + SharerListener listener0, listener2; InviteeListener listener1; @Inject @@ -84,6 +85,7 @@ public class ForumSharingIntegrationTest extends BriarTestCase { // objects accessed from background threads need to be volatile private volatile ForumSharingManager forumSharingManager0; private volatile ForumSharingManager forumSharingManager1; + private volatile ForumSharingManager forumSharingManager2; private volatile Waiter eventWaiter; private volatile Waiter msgWaiter; @@ -92,11 +94,12 @@ public class ForumSharingIntegrationTest extends BriarTestCase { private final int TIMEOUT = 15000; private final String SHARER = "Sharer"; private final String INVITEE = "Invitee"; + private final String SHARER2 = "Sharer2"; private static final Logger LOG = Logger.getLogger(ForumSharingIntegrationTest.class.getName()); - private ForumSharingIntegrationTestComponent t0, t1; + private ForumSharingIntegrationTestComponent t0, t1, t2; @Rule public ExpectedException thrown=ExpectedException.none(); @@ -117,17 +120,26 @@ public class ForumSharingIntegrationTest extends BriarTestCase { t1 = DaggerForumSharingIntegrationTestComponent.builder() .testDatabaseModule(new TestDatabaseModule(t1Dir)).build(); injectEagerSingletons(t1); + File t2Dir = new File(testDir, SHARER2); + t2 = DaggerForumSharingIntegrationTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t2Dir)).build(); + injectEagerSingletons(t2); identityManager0 = t0.getIdentityManager(); identityManager1 = t1.getIdentityManager(); + identityManager2 = t2.getIdentityManager(); contactManager0 = t0.getContactManager(); contactManager1 = t1.getContactManager(); + contactManager2 = t2.getContactManager(); forumManager0 = t0.getForumManager(); forumManager1 = t1.getForumManager(); + forumManager2 = t2.getForumManager(); forumSharingManager0 = t0.getForumSharingManager(); forumSharingManager1 = t1.getForumSharingManager(); + forumSharingManager2 = t2.getForumSharingManager(); sync0 = t0.getSyncSessionFactory(); sync1 = t1.getSyncSessionFactory(); + sync2 = t2.getSyncSessionFactory(); // initialize waiters fresh for each test eventWaiter = new Waiter(); @@ -474,7 +486,8 @@ public class ForumSharingIntegrationTest extends BriarTestCase { assertEquals(1, forumManager1.getForums().size()); // invitee now shares same forum back - forumSharingManager1.sendForumInvitation(forum0.getId(), contactId0, + forumSharingManager1.sendForumInvitation(forum0.getId(), + contactId0, "I am re-sharing this forum with you."); // sync re-share invitation @@ -526,7 +539,9 @@ public class ForumSharingIntegrationTest extends BriarTestCase { // contacts now remove each other contactManager0.removeContact(contactId1); + contactManager2.removeContact(contactId21); contactManager1.removeContact(contactId0); + contactManager1.removeContact(contactId2); // make sure sharer does share the forum with nobody now assertEquals(0, @@ -572,6 +587,75 @@ public class ForumSharingIntegrationTest extends BriarTestCase { } } + @Test + public void testTwoContactsShareSameForum() throws Exception { + startLifecycles(); + try { + // initialize + addDefaultIdentities(); + addDefaultContacts(); + addForumForSharer(); + + // second sharer adds the same forum + DatabaseComponent db2 = t2.getDatabaseComponent(); + Transaction txn = db2.startTransaction(false); + db2.addGroup(txn, forum0.getGroup()); + txn.setComplete(); + db2.endTransaction(txn); + + // add listeners + listener0 = new SharerListener(); + t0.getEventBus().addListener(listener0); + listener1 = new InviteeListener(true, false); + t1.getEventBus().addListener(listener1); + listener2 = new SharerListener(); + t2.getEventBus().addListener(listener2); + + // send invitation + forumSharingManager0 + .sendForumInvitation(forum0.getId(), contactId1, "Hi!"); + // sync first request message + syncToInvitee(); + + // second sharer sends invitation for same forum + forumSharingManager2 + .sendForumInvitation(forum0.getId(), contactId1, null); + // sync second request message + deliverMessage(sync2, contactId2, sync1, contactId1, + "Sharer2 to Invitee"); + + // make sure we have only one forum available + Collection<Forum> forums = + forumSharingManager1.getAvailableForums(); + assertEquals(1, forums.size()); + + // make sure both sharers actually share the forum + Collection<Contact> contacts = + forumSharingManager1.getSharedBy(forum0.getId()); + assertEquals(2, contacts.size()); + + // answer second request + Contact c2 = contactManager1.getContact(contactId2); + forumSharingManager1.respondToInvitation(forum0, c2, true); + // sync response + deliverMessage(sync1, contactId21, sync2, contactId2, + "Invitee to Sharer2"); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener2.responseReceived); + + // answer first request + Contact c0 = + contactManager1.getContact(contactId0); + forumSharingManager1.respondToInvitation(forum0, c0, true); + // sync response + syncToSharer(); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.responseReceived); + } finally { + stopLifecycles(); + } + } + @After public void tearDown() throws InterruptedException { @@ -609,7 +693,8 @@ public class ForumSharingIntegrationTest extends BriarTestCase { requestReceived = true; Forum f = event.getForum(); try { - forumSharingManager0.respondToInvitation(f, true); + Contact c = contactManager0.getContact(contactId1); + forumSharingManager0.respondToInvitation(f, c, true); } catch (DbException ex) { eventWaiter.rethrow(ex); } finally { @@ -624,10 +709,15 @@ public class ForumSharingIntegrationTest extends BriarTestCase { public volatile boolean requestReceived = false; public volatile boolean responseReceived = false; - private final boolean accept; + private final boolean accept, answer; - InviteeListener(boolean accept) { + InviteeListener(boolean accept, boolean answer) { this.accept = accept; + this.answer = answer; + } + + InviteeListener(boolean accept) { + this(accept, true); } public void eventOccurred(Event e) { @@ -644,11 +734,13 @@ public class ForumSharingIntegrationTest extends BriarTestCase { } else if (e instanceof ForumInvitationReceivedEvent) { ForumInvitationReceivedEvent event = (ForumInvitationReceivedEvent) e; - eventWaiter.assertEquals(contactId0, event.getContactId()); requestReceived = true; + if (!answer) return; Forum f = event.getForum(); try { - forumSharingManager1.respondToInvitation(f, accept); + Contact c = + contactManager1.getContact(event.getContactId()); + forumSharingManager1.respondToInvitation(f, c, accept); } catch (DbException ex) { eventWaiter.rethrow(ex); } finally { @@ -670,18 +762,23 @@ public class ForumSharingIntegrationTest extends BriarTestCase { // Start the lifecycle manager and wait for it to finish lifecycleManager0 = t0.getLifecycleManager(); lifecycleManager1 = t1.getLifecycleManager(); + lifecycleManager2 = t2.getLifecycleManager(); lifecycleManager0.startServices(); lifecycleManager1.startServices(); + lifecycleManager2.startServices(); lifecycleManager0.waitForStartup(); lifecycleManager1.waitForStartup(); + lifecycleManager2.waitForStartup(); } private void stopLifecycles() throws InterruptedException { // Clean up lifecycleManager0.stopServices(); lifecycleManager1.stopServices(); + lifecycleManager2.stopServices(); lifecycleManager0.waitForShutdown(); lifecycleManager1.waitForShutdown(); + lifecycleManager2.waitForShutdown(); } private void defaultInit(boolean accept) throws DbException { @@ -700,6 +797,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase { TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH), TestUtils.getRandomBytes(123)); identityManager1.addLocalAuthor(author1); + author2 = authorFactory.createLocalAuthor(SHARER2, + TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + TestUtils.getRandomBytes(123)); + identityManager2.addLocalAuthor(author2); } private void addDefaultContacts() throws DbException { @@ -708,11 +809,20 @@ public class ForumSharingIntegrationTest extends BriarTestCase { author0.getId(), master, clock.currentTimeMillis(), true, true ); - // invitee adds sharer back + // second sharer does the same + contactId21 = contactManager2.addContact(author1, + author2.getId(), master, clock.currentTimeMillis(), true, + true + ); + // invitee adds sharers back contactId0 = contactManager1.addContact(author0, author1.getId(), master, clock.currentTimeMillis(), true, true ); + contactId2 = contactManager1.addContact(author2, + author1.getId(), master, clock.currentTimeMillis(), true, + true + ); } private void addForumForSharer() throws DbException { @@ -725,6 +835,8 @@ public class ForumSharingIntegrationTest extends BriarTestCase { t0.getEventBus().addListener(listener0); listener1 = new InviteeListener(accept); t1.getEventBus().addListener(listener1); + listener2 = new SharerListener(); + t2.getEventBus().addListener(listener2); } private void syncToInvitee() throws IOException, TimeoutException { diff --git a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java index bcf39e8360766b1e722912f3a6c7faec994f4ceb..ed05d2863e38de8d947f730094691ef20954ba89 100644 --- a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java +++ b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java @@ -153,7 +153,7 @@ public class AvailableForumsActivity extends BriarActivity @Override public void onItemClick(AvailableForumsItem item, boolean accept) { - respondToInvitation(item.getForum(), accept); + respondToInvitation(item, accept); // show toast int res = R.string.forum_declined_toast; @@ -161,12 +161,16 @@ public class AvailableForumsActivity extends BriarActivity Toast.makeText(this, res, LENGTH_SHORT).show(); } - private void respondToInvitation(final Forum f, final boolean accept) { - runOnDbThread(new Runnable() { + private void respondToInvitation(final AvailableForumsItem item, + final boolean accept) { + briarController.runOnDbThread(new Runnable() { @Override public void run() { try { - forumSharingManager.respondToInvitation(f, accept); + Forum f = item.getForum(); + for (Contact c : item.getContacts()) { + forumSharingManager.respondToInvitation(f, c, accept); + } } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java index 9f4cc87b6620aebb593a32f3f2e9e3655207aa9a..d20f427f6dc95dea0b23bc3b6bbf6a3e35fb928f 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java +++ b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java @@ -24,7 +24,8 @@ public interface ForumSharingManager { /** * Responds to a pending forum invitation */ - void respondToInvitation(Forum f, boolean accept) throws DbException; + void respondToInvitation(Forum f, Contact c, boolean accept) + throws DbException; /** * Returns all forum sharing messages sent by the Contact diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java index cbeb56d60f2ecf2c14000f897ef145c849bea0c8..01572fbcb6ec221dc7fb933488f0d84e341669d8 100644 --- a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java @@ -55,6 +55,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.api.clients.ProtocolEngine.StateUpdate; import static org.briarproject.api.forum.ForumConstants.CONTACT_ID; +import static org.briarproject.api.forum.ForumConstants.FORUM_ID; import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; import static org.briarproject.api.forum.ForumConstants.IS_SHARER; import static org.briarproject.api.forum.ForumConstants.LOCAL; @@ -67,6 +68,7 @@ import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT; import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE; import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE; +import static org.briarproject.api.forum.ForumConstants.STATE; import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US; import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US; import static org.briarproject.api.forum.ForumConstants.TASK_ADD_SHARED_FORUM; @@ -155,16 +157,18 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook @Override public void removingContact(Transaction txn, Contact c) throws DbException { + // query for this contact c + BdfDictionary query = BdfDictionary.of( + new BdfEntry(CONTACT_ID, c.getId().getInt()) + ); + // clean up session states with that contact from localGroup - Long id = (long) c.getId().getInt(); try { Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId()); + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) { - BdfDictionary d = entry.getValue(); - if (id.equals(d.getLong(CONTACT_ID))) { - deleteMessage(txn, entry.getKey()); - } + deleteMessage(txn, entry.getKey()); } } catch (FormatException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -279,13 +283,14 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook } @Override - public void respondToInvitation(Forum f, boolean accept) + public void respondToInvitation(Forum f, Contact c, boolean accept) throws DbException { Transaction txn = db.startTransaction(false); try { // find session state based on forum - InviteeSessionState localState = getSessionStateForResponse(txn, f); + InviteeSessionState localState = + getSessionStateForResponse(txn, f, c); // define action InviteeSessionState.Action localAction; @@ -312,6 +317,11 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook public Collection<ForumInvitationMessage> getForumInvitationMessages( ContactId contactId) throws DbException { + // query for all invitations + BdfDictionary query = BdfDictionary.of( + new BdfEntry(TYPE, SHARE_MSG_TYPE_INVITATION) + ); + Transaction txn = db.startTransaction(false); try { Contact contact = db.getContact(txn, contactId); @@ -320,13 +330,10 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook Collection<ForumInvitationMessage> list = new ArrayList<ForumInvitationMessage>(); Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, group.getId()); + .getMessageMetadataAsDictionary(txn, group.getId(), query); for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { BdfDictionary d = m.getValue(); try { - if (d.getLong(TYPE) != SHARE_MSG_TYPE_INVITATION) - continue; - Invitation msg = Invitation.from(group.getId(), d); MessageStatus status = db.getMessageStatus(txn, contactId, m.getKey()); @@ -542,23 +549,29 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook try { return getSessionStateForSharer(txn, sessionId); } catch (NoSuchMessageException e) { - // State not found directly, so iterate over all states - // to find state for invitee + // State not found directly, so query for state for invitee + BdfDictionary query = BdfDictionary.of( + new BdfEntry(SESSION_ID, sessionId) + ); + Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId()); - for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { - BdfDictionary state = m.getValue(); - if (Arrays.equals(state.getRaw(SESSION_ID), - sessionId.getBytes())) { - return fromBdfDictionary(state); - } - } - if (warn && LOG.isLoggable(WARNING)) { + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); + + if (map.size() > 1 && LOG.isLoggable(WARNING)) { LOG.warning( - "No session state found for message with session ID " + + "More than one session state found for message with session ID " + Arrays.hashCode(sessionId.getBytes())); } - throw new FormatException(); + if (map.isEmpty()) { + if (warn && LOG.isLoggable(WARNING)) { + LOG.warning( + "No session state found for message with session ID " + + Arrays.hashCode(sessionId.getBytes())); + } + throw new FormatException(); + } + return fromBdfDictionary(map.values().iterator().next()); } } @@ -576,50 +589,55 @@ class ForumSharingManagerImpl extends BdfIncomingMessageHook } private InviteeSessionState getSessionStateForResponse(Transaction txn, - Forum f) throws DbException, FormatException { + Forum f, Contact c) throws DbException, FormatException { + + // query for invitee states for that forum in state await response + BdfDictionary query = BdfDictionary.of( + new BdfEntry(IS_SHARER, false), + new BdfEntry(CONTACT_ID, c.getId().getInt()), + new BdfEntry(FORUM_ID, f.getId()), + new BdfEntry(STATE, + InviteeSessionState.State.AWAIT_LOCAL_RESPONSE + .getValue()) + ); Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId()); - for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { - BdfDictionary d = m.getValue(); - try { - ForumSharingSessionState s = fromBdfDictionary(d); - if (!(s instanceof InviteeSessionState)) continue; - if (!f.getId().equals(s.getForumId())) continue; - - InviteeSessionState state = (InviteeSessionState) s; - if (state.getState() == - InviteeSessionState.State.AWAIT_LOCAL_RESPONSE) { - // Note that there should always be only one session - // in this state for the same forum - return state; - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); + + if (map.size() > 1 && LOG.isLoggable(WARNING)) { + LOG.warning( + "More than one session state found for forum with ID " + + Arrays.hashCode(f.getId().getBytes()) + + " in state AWAIT_LOCAL_RESPONSE for contact " + + c.getAuthor().getName()); + } + if (map.isEmpty()) { + if (LOG.isLoggable(WARNING)) { + LOG.warning( + "No session state found for forum with ID " + + Arrays.hashCode(f.getId().getBytes()) + + " in state AWAIT_LOCAL_RESPONSE"); } + throw new DbException(); } - throw new DbException(); + return (InviteeSessionState) fromBdfDictionary( + map.values().iterator().next()); } private ForumSharingSessionState getSessionStateForLeaving(Transaction txn, Forum f, ContactId c) throws DbException, FormatException { + BdfDictionary query = BdfDictionary.of( + new BdfEntry(CONTACT_ID, c.getInt()), + new BdfEntry(FORUM_ID, f.getId()) + ); Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, localGroup.getId()); + .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { BdfDictionary d = m.getValue(); try { ForumSharingSessionState s = fromBdfDictionary(d); - // check that this session is with the right contact - if (!c.equals(s.getContactId())) continue; - - // check that this state actually concerns this forum - if (!s.getForumName().equals(f.getName()) || - !Arrays.equals(s.getForumSalt(), f.getSalt())) { - continue; - } - // check that a forum get be left in current session if (s instanceof SharerSessionState) { SharerSessionState state = (SharerSessionState) s;