diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java index 7f2f02be254eda522d1850d2cc47f1c5c6248f09..ca1026084599207098f3efb79076038f96a342f3 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java @@ -50,6 +50,7 @@ import javax.inject.Inject; import static org.briarproject.TestPluginsModule.MAX_LATENCY; import static org.briarproject.TestPluginsModule.TRANSPORT_ID; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -293,7 +294,19 @@ public class IntroductionIntegrationTest extends BriarTestCase { assertFalse(contactManager2 .contactExists(author1.getId(), author2.getId())); - assertDefaultUiMessages(); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId1) + .size()); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId2) + .size()); + assertEquals(2, + introductionManager1.getIntroductionMessages(contactId0) + .size()); + // introducee2 should also have the decline response of introducee1 + assertEquals(3, + introductionManager2.getIntroductionMessages(contactId0) + .size()); } finally { stopLifecycles(); } @@ -369,6 +382,97 @@ public class IntroductionIntegrationTest extends BriarTestCase { assertFalse(contactManager2 .contactExists(author1.getId(), author2.getId())); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId1) + .size()); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId2) + .size()); + // introducee1 also sees the decline response from introducee2 + assertEquals(3, + introductionManager1.getIntroductionMessages(contactId0) + .size()); + assertEquals(2, + introductionManager2.getIntroductionMessages(contactId0) + .size()); + } finally { + stopLifecycles(); + } + } + + @Test + public void testIntroductionSessionDelayedFirstDecline() throws Exception { + startLifecycles(); + try { + // Add Identities + addDefaultIdentities(); + + // Add Transport Properties + addTransportProperties(); + + // Add introducees as contacts + contactId1 = contactManager0.addContact(author1, author0.getId(), + master, clock.currentTimeMillis(), true, true + ); + contactId2 = contactManager0.addContact(author2, author0.getId(), + master, clock.currentTimeMillis(), true, true + ); + // Add introducer back + contactId0 = contactManager1.addContact(author0, author1.getId(), + master, clock.currentTimeMillis(), false, true + ); + ContactId contactId02 = contactManager2.addContact(author0, + author2.getId(), master, clock.currentTimeMillis(), false, + true + ); + assertTrue(contactId0.equals(contactId02)); + + // listen to events + IntroducerListener listener0 = new IntroducerListener(); + t0.getEventBus().addListener(listener0); + IntroduceeListener listener1 = new IntroduceeListener(1, false); + t1.getEventBus().addListener(listener1); + IntroduceeListener listener2 = new IntroduceeListener(2, false); + t2.getEventBus().addListener(listener2); + + // make introduction + long time = clock.currentTimeMillis(); + Contact introducee1 = contactManager0.getContact(contactId1); + Contact introducee2 = contactManager0.getContact(contactId2); + introductionManager0 + .makeIntroduction(introducee1, introducee2, null, time); + + // sync request messages + deliverMessage(sync0, contactId0, sync1, contactId1); + deliverMessage(sync0, contactId0, sync2, contactId2); + + // wait for requests to arrive + eventWaiter.await(TIMEOUT, 2); + assertTrue(listener1.requestReceived); + assertTrue(listener2.requestReceived); + + // sync first response + deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.response1Received); + + // sync second response + deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.response2Received); + + // sync first forwarded response + deliverMessage(sync0, contactId0, sync2, contactId2); + + // note how the second response will not be forwarded anymore + + assertFalse(contactManager1 + .contactExists(author2.getId(), author1.getId())); + assertFalse(contactManager2 + .contactExists(author1.getId(), author2.getId())); + + // since introducee2 was already in FINISHED state when + // introducee1's response arrived, she ignores and deletes it assertDefaultUiMessages(); } finally { stopLifecycles(); @@ -614,14 +718,18 @@ public class IntroductionIntegrationTest extends BriarTestCase { } private void assertDefaultUiMessages() throws DbException { - assertTrue(introductionManager0.getIntroductionMessages(contactId1) - .size() == 2); - assertTrue(introductionManager0.getIntroductionMessages(contactId2) - .size() == 2); - assertTrue(introductionManager1.getIntroductionMessages(contactId0) - .size() == 2); - assertTrue(introductionManager2.getIntroductionMessages(contactId0) - .size() == 2); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId1) + .size()); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId2) + .size()); + assertEquals(2, + introductionManager1.getIntroductionMessages(contactId0) + .size()); + assertEquals(2, + introductionManager2.getIntroductionMessages(contactId0) + .size()); } private class IntroduceeListener implements EventListener { diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index ca1e962f4e95be6e1de366ec80c8a9edc9252721..e4452b1bc6698a32f7a4f7b84ecf2eed9ff3aa89 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -161,6 +161,7 @@ <string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string> <string name="introduction_response_accepted_received">%1$s accepted to be introduced to %2$s.</string> <string name="introduction_response_declined_received">%1$s declined to be introduced to %2$s.</string> + <string name="introduction_response_declined_received_by_introducee">%1$s has informed us that %2$s has declined the introduction.</string> <string name="introduction_success_title">Introduced contact was added</string> <string name="introduction_success_text">You have been introduced to %1$s.</string> diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java index 2c1492a8c3ee05706023f12dfa415a589272fb7b..76eb803a35f96f02c9df891af873d6605f08f2e4 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -77,9 +77,15 @@ public abstract class ConversationItem { R.string.introduction_response_accepted_received, contactName, ir.getName()); } else { - text = ctx.getString( - R.string.introduction_response_declined_received, - contactName, ir.getName()); + if (ir.isIntroducer()) { + text = ctx.getString( + R.string.introduction_response_declined_received, + contactName, ir.getName()); + } else { + text = ctx.getString( + R.string.introduction_response_declined_received_by_introducee, + contactName, ir.getName()); + } } return new ConversationNoticeInItem(ir.getMessageId(), text, ir.getTime(), ir.isRead()); diff --git a/briar-api/src/org/briarproject/api/ProtocolEngine.java b/briar-api/src/org/briarproject/api/ProtocolEngine.java index 65d9fe52e178ac54903c2694cae6d58bf88411e2..61833f6574ec628b33b9195adc4e17c49206840d 100644 --- a/briar-api/src/org/briarproject/api/ProtocolEngine.java +++ b/briar-api/src/org/briarproject/api/ProtocolEngine.java @@ -12,16 +12,27 @@ public interface ProtocolEngine<A, S, M> { StateUpdate<S, M> onMessageDelivered(S localState, M delivered); class StateUpdate<S, M> { - public final boolean deleteMessages; + public final boolean deleteMessage; public final boolean deleteState; public final S localState; public final List<M> toSend; public final List<Event> toBroadcast; - public StateUpdate(boolean deleteMessages, boolean deleteState, + /** + * This class represents an update of the local protocol state. + * It only shows how the state should be updated, + * but does not carry out the updates on its own. + * + * @param deleteMessage whether to delete the message that triggered the state update. This will be ignored for {@link ProtocolEngine#onLocalAction}. + * @param deleteState whether to delete the localState {@link S} + * @param localState the new local state + * @param toSend a list of messages to be sent as part of the state update + * @param toBroadcast a list of events to broadcast as result of the state update + */ + public StateUpdate(boolean deleteMessage, boolean deleteState, S localState, List<M> toSend, List<Event> toBroadcast) { - this.deleteMessages = deleteMessages; + this.deleteMessage = deleteMessage; this.deleteState = deleteState; this.localState = localState; this.toSend = toSend; diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java index 6ac98793f208356cd66a3b4f6c41004f70727bc6..8582f36c6ddc0303fcd6e23b05205aae7f6b6518 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java @@ -2,19 +2,24 @@ package org.briarproject.api.introduction; import org.briarproject.api.sync.MessageId; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER; + abstract public class IntroductionMessage { private final SessionId sessionId; private final MessageId messageId; + private final int role; private final long time; private final boolean local, sent, seen, read; public IntroductionMessage(SessionId sessionId, MessageId messageId, - long time, boolean local, boolean sent, boolean seen, + int role, long time, boolean local, boolean sent, boolean seen, boolean read) { this.sessionId = sessionId; this.messageId = messageId; + this.role = role; this.time = time; this.local = local; this.sent = sent; @@ -50,5 +55,13 @@ abstract public class IntroductionMessage { return read; } + public boolean isIntroducer() { + return role == ROLE_INTRODUCER; + } + + public boolean isIntroducee() { + return role == ROLE_INTRODUCEE; + } + } diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java index 163eb57be514cd0f372343ec95f97f071ec98afc..dc674f77b9f950bad7128ff1db95080cb53eb385 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java @@ -9,12 +9,13 @@ public class IntroductionRequest extends IntroductionResponse { private final boolean answered, exists, introducesOtherIdentity; public IntroductionRequest(SessionId sessionId, MessageId messageId, - long time, boolean local, boolean sent, boolean seen, boolean read, - AuthorId authorId, String name, boolean accepted, String message, - boolean answered, boolean exists, boolean introducesOtherIdentity) { + int role, long time, boolean local, boolean sent, boolean seen, + boolean read, AuthorId authorId, String name, boolean accepted, + String message, boolean answered, boolean exists, + boolean introducesOtherIdentity) { - super(sessionId, messageId, time, local, sent, seen, read, authorId, - name, accepted); + super(sessionId, messageId, role, time, local, sent, seen, read, + authorId, name, accepted); this.message = message; this.answered = answered; diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java index e73065bcc0800ea4d874521e84ed3fb8dafd2bcd..956131ccb54a289841a33f2c488cad061c5fb965 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java @@ -10,10 +10,11 @@ public class IntroductionResponse extends IntroductionMessage { private final boolean accepted; public IntroductionResponse(SessionId sessionId, MessageId messageId, - long time, boolean local, boolean sent, boolean seen, boolean read, - AuthorId remoteAuthorId, String name, boolean accepted) { + int role, long time, boolean local, boolean sent, boolean seen, + boolean read, AuthorId remoteAuthorId, String name, + boolean accepted) { - super(sessionId, messageId, time, local, sent, seen, read); + super(sessionId, messageId, role, time, local, sent, seen, read); this.remoteAuthorId = remoteAuthorId; this.name = name; @@ -28,4 +29,7 @@ public class IntroductionResponse extends IntroductionMessage { return accepted; } + public AuthorId getRemoteAuthorId() { + return remoteAuthorId; + } } diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java index ad6e005f6e69426056e8cdac760f4bc1746ecd25..cb1ee7e023f66cdc316252f4bf29dc69996fe67b 100644 --- a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java +++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java @@ -51,6 +51,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME; import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; import static org.briarproject.api.introduction.IntroductionConstants.STATE; import static org.briarproject.api.introduction.IntroductionConstants.TASK; @@ -195,14 +196,11 @@ public class IntroduceeEngine messages = Collections.emptyList(); events = Collections.emptyList(); } - // we are done (probably declined response) and ignore this message + // we are done (probably declined response), ignore & delete message else if (currentState == FINISHED) { - if(action == REMOTE_DECLINE || action == REMOTE_ACCEPT) { - // record response data, - // so we later know which response was ours - addResponseData(localState, msg); - } - return noUpdate(localState); + return new StateUpdate<BdfDictionary, BdfDictionary>(true, + false, localState, new ArrayList<BdfDictionary>(0), + new ArrayList<Event>(0)); } // this should not happen else { @@ -341,8 +339,8 @@ public class IntroduceeEngine localState.getBoolean(REMOTE_AUTHOR_IS_US); IntroductionRequest ir = new IntroductionRequest(sessionId, messageId, - time, false, false, false, false, authorId, name, false, - message, false, exists, introducesOtherIdentity); + ROLE_INTRODUCEE, time, false, false, false, false, authorId, + name, false, message, false, exists, introducesOtherIdentity); return new IntroductionRequestReceivedEvent(contactId, ir); } diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java index b329a3abf14c3d1f14347de802d4dd2e91ae2a7b..c0f9506f56633082d8b4fb2cb1fb64dc7f8cfef0 100644 --- a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java +++ b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java @@ -1,6 +1,5 @@ package org.briarproject.introduction; - import org.briarproject.api.Bytes; import org.briarproject.api.FormatException; import org.briarproject.api.TransportId; @@ -39,6 +38,7 @@ import java.util.logging.Logger; import javax.inject.Inject; +import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST; import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; @@ -51,6 +51,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_K import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER; import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID; import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; import static org.briarproject.api.introduction.IntroductionConstants.NAME; import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; @@ -170,7 +171,7 @@ class IntroduceeManager { BdfDictionary message) throws DbException, FormatException { IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, engine.onMessageReceived(state, message)); + processStateUpdate(txn, message, engine.onMessageReceived(state, message)); } public void acceptIntroduction(Transaction txn, BdfDictionary state, @@ -200,7 +201,7 @@ class IntroduceeManager { // start engine and process its state update IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, engine.onLocalAction(state, localAction)); + processStateUpdate(txn, null, engine.onLocalAction(state, localAction)); } public void declineIntroduction(Transaction txn, BdfDictionary state, @@ -217,11 +218,11 @@ class IntroduceeManager { // start engine and process its state update IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, + processStateUpdate(txn, null, engine.onLocalAction(state, localAction)); } - private void processStateUpdate(Transaction txn, + private void processStateUpdate(Transaction txn, BdfDictionary msg, IntroduceeEngine.StateUpdate<BdfDictionary, BdfDictionary> result) throws DbException, FormatException { @@ -242,6 +243,16 @@ class IntroduceeManager { for (Event event : result.toBroadcast) { txn.attach(event); } + + // delete message + if (result.deleteMessage && msg != null) { + MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); + if (LOG.isLoggable(INFO)) { + LOG.info("Deleting message with id " + messageId.hashCode()); + } + db.deleteMessage(txn, messageId); + db.deleteMessageMetadata(txn, messageId); + } } private void performTasks(Transaction txn, BdfDictionary localState) @@ -253,8 +264,6 @@ class IntroduceeManager { long task = localState.getLong(TASK); localState.put(TASK, BdfDictionary.NULL_VALUE); - - if (task == TASK_ADD_CONTACT) { if (localState.getBoolean(EXISTS)) { // we have this contact already, so do not perform actions @@ -374,7 +383,7 @@ class IntroduceeManager { BdfDictionary localAction = new BdfDictionary(); localAction.put(TYPE, TYPE_ABORT); try { - processStateUpdate(txn, + processStateUpdate(txn, null, engine.onLocalAction(state, localAction)); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java index 6beacb934456e2fecda959f7e1c627a7baa4ea93..6af05d14bc3afd74a19631d05d485e459d34cfda 100644 --- a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java +++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java @@ -55,6 +55,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2; import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1; import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER; import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; import static org.briarproject.api.introduction.IntroductionConstants.STATE; import static org.briarproject.api.introduction.IntroductionConstants.TYPE; @@ -302,8 +303,9 @@ public class IntroducerEngine boolean accept = msg.getBoolean(ACCEPT); IntroductionResponse ir = - new IntroductionResponse(sessionId, messageId, time, false, - false, false, false, authorId, name, accept); + new IntroductionResponse(sessionId, messageId, ROLE_INTRODUCER, + time, false, false, false, false, authorId, name, + accept); return new IntroductionResponseReceivedEvent(contactId, ir); } diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java index 24c89324e1ee33ee1beb398b4e4c01814f3df7be..0d0c3102bd609708baec13d068ff3324bd328023 100644 --- a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java +++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java @@ -41,6 +41,7 @@ import java.util.logging.Logger; import javax.inject.Inject; import static java.util.logging.Level.WARNING; +import static org.briarproject.api.introduction.IntroduceeProtocolState.FINISHED; import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED; import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1; @@ -331,6 +332,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook BdfDictionary state = getSessionState(txn, g, sessionId.getBytes()); + int role = state.getLong(ROLE).intValue(); boolean local; long time = msg.getLong(MESSAGE_TIME); boolean accepted = msg.getBoolean(ACCEPT, false); @@ -338,7 +340,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook AuthorId authorId; String name; if (type == TYPE_RESPONSE) { - if (state.getLong(ROLE) == ROLE_INTRODUCER) { + if (role == ROLE_INTRODUCER) { if (!concernsThisContact(contactId, messageId, state)) { // this response is not from contactId continue; @@ -350,22 +352,30 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook } else { if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE), messageId.getBytes())) { - // this response is not ours, don't include it - continue; + // this response is not ours, + // check if it was a decline + if (!accepted) { + local = false; + } else { + // don't include positive responses + continue; + } + } else { + local = true; } - local = true; authorId = new AuthorId( state.getRaw(REMOTE_AUTHOR_ID)); name = state.getString(NAME); } IntroductionResponse ir = new IntroductionResponse( - sessionId, messageId, time, local, s.isSent(), - s.isSeen(), read, authorId, name, accepted); + sessionId, messageId, role, time, local, + s.isSent(), s.isSeen(), read, authorId, name, + accepted); list.add(ir); } else if (type == TYPE_REQUEST) { String message; boolean answered, exists, introducesOtherIdentity; - if (state.getLong(ROLE) == ROLE_INTRODUCER) { + if (role == ROLE_INTRODUCER) { local = true; authorId = getAuthorIdForIntroducer(contactId, state); @@ -380,15 +390,17 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook state.getRaw(REMOTE_AUTHOR_ID)); name = state.getString(NAME); message = state.getOptionalString(MSG); - answered = state.getBoolean(ANSWERED); + boolean finished = state.getLong(STATE) == + FINISHED.getValue(); + answered = finished || state.getBoolean(ANSWERED); exists = state.getBoolean(EXISTS); introducesOtherIdentity = state.getBoolean(REMOTE_AUTHOR_IS_US); } IntroductionRequest ir = new IntroductionRequest( - sessionId, messageId, time, local, s.isSent(), - s.isSeen(), read, authorId, name, accepted, - message, answered, exists, + sessionId, messageId, role, time, local, + s.isSent(), s.isSeen(), read, authorId, name, + accepted, message, answered, exists, introducesOtherIdentity); list.add(ir); }