Merge branch '392-use-new-metadata-queries-for-forum-sharing' into 'master'

Handle invitations to the same forum by multiple contacts

This also uses the new metadata queries in the forum sharing client.

Please note that this is based on !184.

See merge request !188
This commit is contained in:
Torsten Grote
2016-05-19 14:35:58 +00:00
4 changed files with 210 additions and 75 deletions

View File

@@ -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, boolean answer) {
this.accept = accept;
this.answer = answer;
}
InviteeListener(boolean accept) {
this.accept = 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 {

View File

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

View File

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

View File

@@ -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;
.getMessageMetadataAsDictionary(txn, localGroup.getId(), query);
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);
}
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());
}
throw new DbException();
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();
}
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;