diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
index 39dcb24a4..9070c738f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
@@ -124,6 +124,9 @@ abstract class ConversationItem {
text = ctx.getString(
R.string.introduction_response_accepted_sent,
ir.getName());
+ text += "\n\n" + ctx.getString(
+ R.string.introduction_response_accepted_sent_info,
+ ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 38b04cd53..d77582999 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -156,6 +156,7 @@
%1$s has asked to introduce you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond:
%1$s has asked to introduce you to %2$s.
You accepted the introduction to %1$s.
+ Before %1$s gets added to your contacts, they need to accept the introduction as well. This might take some time.
You declined the introduction to %1$s.
%1$s accepted the introduction to %2$s.
%1$s declined the introduction to %2$s.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
index 8711193f1..9a267c5c2 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
@@ -25,6 +25,9 @@ public interface IntroductionManager extends ConversationClient {
*/
int CLIENT_VERSION = 1;
+ /**
+ * Returns true if both contacts can be introduced at this moment.
+ */
boolean canIntroduce(Contact c1, Contact c2) throws DbException;
/**
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
index ae395b596..03b3486ad 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
@@ -144,15 +144,15 @@ abstract class AbstractProtocolEngine
}
}
- void broadcastIntroductionResponseReceivedEvent(Transaction txn,
- Session s, AuthorId sender, AbstractIntroductionMessage m)
+ void broadcastIntroductionResponseReceivedEvent(Transaction txn, Session s,
+ AuthorId sender, Author otherAuthor, AbstractIntroductionMessage m)
throws DbException {
AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
Contact c = contactManager.getContact(txn, sender, localAuthorId);
IntroductionResponse response =
new IntroductionResponse(s.getSessionId(), m.getMessageId(),
m.getGroupId(), s.getRole(), m.getTimestamp(), false,
- false, false, false, c.getAuthor().getName(),
+ false, false, false, otherAuthor.getName(),
m instanceof AcceptMessage);
IntroductionResponseReceivedEvent e =
new IntroductionResponseReceivedEvent(c.getId(), response);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
index 9f7f83ab6..e2ea32ed6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
@@ -355,7 +355,7 @@ class IntroduceeProtocolEngine
// Broadcast IntroductionResponseReceivedEvent
broadcastIntroductionResponseReceivedEvent(txn, s,
- s.getIntroducer().getId(), m);
+ s.getIntroducer().getId(), s.getRemote().author, m);
if (s.getState() == AWAIT_RESPONSES) {
// Mark the request message unavailable to answer
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
index 50b9d01a9..a346a0bf3 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
@@ -261,19 +261,24 @@ class IntroducerProtocolEngine
// Create the next state
IntroducerState state = AWAIT_AUTHS;
Introducee introduceeA, introduceeB;
+ Author sender, other;
if (senderIsAlice) {
if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B;
introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
introduceeB = new Introducee(s.getIntroduceeB(), sent);
+ sender = introduceeA.author;
+ other = introduceeB.author;
} else {
if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A;
introduceeA = new Introducee(s.getIntroduceeA(), sent);
introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+ sender = introduceeB.author;
+ other = introduceeA.author;
}
// Broadcast IntroductionResponseReceivedEvent
- Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
- broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
+ broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(),
+ other, m);
// Move to the next state
return new IntroducerSession(s.getSessionId(), state,
@@ -313,17 +318,22 @@ class IntroducerProtocolEngine
m.getTransportProperties(), false);
Introducee introduceeA, introduceeB;
+ Author sender, other;
if (senderIsAlice) {
introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
introduceeB = new Introducee(s.getIntroduceeB(), sent);
+ sender = introduceeA.author;
+ other = introduceeB.author;
} else {
introduceeA = new Introducee(s.getIntroduceeA(), sent);
introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+ sender = introduceeB.author;
+ other = introduceeA.author;
}
// Broadcast IntroductionResponseReceivedEvent
- Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
- broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
+ broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(),
+ other, m);
return new IntroducerSession(s.getSessionId(), START,
s.getRequestTimestamp(), introduceeA, introduceeB);
@@ -360,19 +370,24 @@ class IntroducerProtocolEngine
// Create the next state
IntroducerState state = START;
Introducee introduceeA, introduceeB;
+ Author sender, other;
if (senderIsAlice) {
if (s.getState() == AWAIT_RESPONSES) state = A_DECLINED;
introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
introduceeB = new Introducee(s.getIntroduceeB(), sent);
+ sender = introduceeA.author;
+ other = introduceeB.author;
} else {
if (s.getState() == AWAIT_RESPONSES) state = B_DECLINED;
introduceeA = new Introducee(s.getIntroduceeA(), sent);
introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+ sender = introduceeB.author;
+ other = introduceeA.author;
}
// Broadcast IntroductionResponseReceivedEvent
- Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
- broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
+ broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(),
+ other, m);
return new IntroducerSession(s.getSessionId(), state,
s.getRequestTimestamp(), introduceeA, introduceeB);
@@ -405,17 +420,22 @@ class IntroducerProtocolEngine
Message sent = sendDeclineMessage(txn, i, timestamp, false);
Introducee introduceeA, introduceeB;
+ Author sender, other;
if (senderIsAlice) {
introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
introduceeB = new Introducee(s.getIntroduceeB(), sent);
+ sender = introduceeA.author;
+ other = introduceeB.author;
} else {
introduceeA = new Introducee(s.getIntroduceeA(), sent);
introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+ sender = introduceeB.author;
+ other = introduceeA.author;
}
// Broadcast IntroductionResponseReceivedEvent
- Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
- broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
+ broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(),
+ other, m);
return new IntroducerSession(s.getSessionId(), START,
s.getRequestTimestamp(), introduceeA, introduceeB);
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index df0d46b88..b8e5b646b 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -28,6 +28,7 @@ import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.introduction.IntroductionMessage;
import org.briarproject.briar.api.introduction.IntroductionRequest;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent;
import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
@@ -51,6 +52,7 @@ import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES;
import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED;
import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED;
import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED;
@@ -146,24 +148,32 @@ public class IntroductionIntegrationTest
sync0To1(1, true);
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener1.requestReceived);
+ assertEquals(introducee2.getAuthor().getName(),
+ listener1.getRequest().getName());
assertGroupCount(messageTracker1, g1.getId(), 2, 1);
// sync second REQUEST message
sync0To2(1, true);
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener2.requestReceived);
+ assertEquals(introducee1.getAuthor().getName(),
+ listener2.getRequest().getName());
assertGroupCount(messageTracker2, g2.getId(), 2, 1);
// sync first ACCEPT message
sync1To0(1, true);
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response1Received);
+ assertEquals(introducee2.getAuthor().getName(),
+ listener0.getResponse().getName());
assertGroupCount(messageTracker0, g1.getId(), 2, 1);
// sync second ACCEPT message
sync2To0(1, true);
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response2Received);
+ assertEquals(introducee1.getAuthor().getName(),
+ listener0.getResponse().getName());
assertGroupCount(messageTracker0, g2.getId(), 2, 1);
// sync forwarded ACCEPT messages to introducees
@@ -259,6 +269,10 @@ public class IntroductionIntegrationTest
assertEquals(alice ? A_DECLINED : B_DECLINED,
introducerSession.getState());
+ // assert that the name on the decline event is correct
+ assertEquals(introducee2.getAuthor().getName(),
+ listener0.getResponse().getName());
+
// sync second response
sync2To0(1, true);
eventWaiter.await(TIMEOUT, 1);
@@ -271,6 +285,11 @@ public class IntroductionIntegrationTest
// sync first forwarded response
sync0To2(1, true);
+ // assert that the name on the decline event is correct
+ eventWaiter.await(TIMEOUT, 1);
+ assertEquals(introducee1.getAuthor().getName(),
+ listener2.getResponse().getName());
+
// note how the introducer does not forward the second response,
// because after the first decline the protocol finished
@@ -339,6 +358,11 @@ public class IntroductionIntegrationTest
sync0To2(1, true);
sync0To1(1, true);
+ // assert that the name on the decline event is correct
+ eventWaiter.await(TIMEOUT, 1);
+ assertEquals(contact2From0.getAuthor().getName(),
+ listener1.getResponse().getName());
+
assertFalse(contactManager1
.contactExists(author2.getId(), author1.getId()));
assertFalse(contactManager2
@@ -408,8 +432,6 @@ public class IntroductionIntegrationTest
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();
assertFalse(listener0.aborted);
assertFalse(listener1.aborted);
@@ -417,7 +439,7 @@ public class IntroductionIntegrationTest
}
@Test
- public void testResponseAndAckInOneSession() throws Exception {
+ public void testResponseAndAuthInOneSync() throws Exception {
addListeners(true, true);
// make introduction
@@ -449,10 +471,125 @@ public class IntroductionIntegrationTest
.respondToIntroduction(contactId0From2, listener2.sessionId, time,
true);
- // sync second response and ACK and make sure there is no abort
+ // sync second response and AUTH
sync2To0(2, true);
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response2Received);
+
+ // Forward AUTH
+ sync0To1(1, true);
+
+ // Second AUTH and ACTIATE and forward them
+ sync1To0(2, true);
+ sync0To2(2, true);
+
+ assertTrue(contactManager1
+ .contactExists(author2.getId(), author1.getId()));
+ assertTrue(contactManager2
+ .contactExists(author1.getId(), author2.getId()));
+
+ assertDefaultUiMessages();
+ assertFalse(listener0.aborted);
+ assertFalse(listener1.aborted);
+ assertFalse(listener2.aborted);
+ }
+
+ /**
+ * When an introducee declines an introduction,
+ * the other introducee needs to respond before returning to START state,
+ * otherwise a subsequent attempt at introducing the same contacts will fail
+ */
+ @Test
+ public void testAutomaticSecondDecline() throws Exception {
+ // introducee1 declines automatically and introducee2 doesn't answer
+ addListeners(false, true);
+ listener2.answerRequests = false;
+
+ // make introduction
+ long time = clock.currentTimeMillis();
+ Contact introducee1 = contact1From0;
+ Contact introducee2 = contact2From0;
+ introductionManager0
+ .makeIntroduction(introducee1, introducee2, null, time);
+
+ // sync request messages
+ sync0To1(1, true);
+ sync0To2(1, true);
+
+ // assert that introducee1 is in correct state
+ IntroduceeSession introduceeSession = getIntroduceeSession(c1);
+ assertEquals(LOCAL_DECLINED, introduceeSession.getState());
+
+ // sync first response
+ sync1To0(1, true);
+ eventWaiter.await(TIMEOUT, 1);
+ assertTrue(listener0.response1Received);
+
+ // assert that introducer is in correct state
+ boolean alice = c0.getIntroductionCrypto()
+ .isAlice(introducee1.getAuthor().getId(),
+ introducee2.getAuthor().getId());
+ IntroducerSession introducerSession = getIntroducerSession();
+ assertEquals(alice ? A_DECLINED : B_DECLINED,
+ introducerSession.getState());
+
+ // assert that introducee2 is in correct state
+ introduceeSession = getIntroduceeSession(c2);
+ assertEquals(AWAIT_RESPONSES, introduceeSession.getState());
+
+ // forward first DECLINE
+ sync0To2(1, true);
+
+ // assert that the name on the decline event is correct
+ eventWaiter.await(TIMEOUT, 1);
+ assertEquals(introducee1.getAuthor().getName(),
+ listener2.getResponse().getName());
+
+ // assert that introducee2 is in correct state
+ introduceeSession = getIntroduceeSession(c2);
+ assertEquals(IntroduceeState.START, introduceeSession.getState());
+
+ // second response should be an immediate automatic DECLINE
+ sync2To0(1, true);
+ eventWaiter.await(TIMEOUT, 1);
+ assertTrue(listener0.response2Received);
+
+ // assert that introducer now moved to START state
+ introducerSession = getIntroducerSession();
+ assertEquals(START, introducerSession.getState());
+
+ // introducee1 is still waiting for second response
+ introduceeSession = getIntroduceeSession(c1);
+ assertEquals(LOCAL_DECLINED, introduceeSession.getState());
+
+ // forward automatic decline
+ sync0To1(1, true);
+
+ // introducee1 can finally move to the START
+ introduceeSession = getIntroduceeSession(c1);
+ assertEquals(IntroduceeState.START, introduceeSession.getState());
+
+ Group g1 = introductionManager0.getContactGroup(introducee1);
+ Group g2 = introductionManager0.getContactGroup(introducee2);
+ assertEquals(2,
+ introductionManager0.getIntroductionMessages(contactId1From0)
+ .size());
+ assertGroupCount(messageTracker0, g1.getId(), 2, 1);
+ assertEquals(2,
+ introductionManager0.getIntroductionMessages(contactId2From0)
+ .size());
+ assertGroupCount(messageTracker0, g2.getId(), 2, 1);
+ assertEquals(2,
+ introductionManager1.getIntroductionMessages(contactId0From1)
+ .size());
+ assertGroupCount(messageTracker1, g1.getId(), 2, 1);
+ // the automatic DECLINE is invisible in the UI
+ // so there's just the remote REQUEST and remote DECLINE
+ assertEquals(2,
+ introductionManager2.getIntroductionMessages(contactId0From2)
+ .size());
+ assertGroupCount(messageTracker2, g2.getId(), 2, 2);
+
assertFalse(listener0.aborted);
assertFalse(listener1.aborted);
assertFalse(listener2.aborted);
@@ -1012,11 +1149,25 @@ public class IntroductionIntegrationTest
@MethodsNotNullByDefault
@ParametersNotNullByDefault
- private class IntroduceeListener implements EventListener {
+ private abstract class IntroductionListener implements EventListener {
+
+ protected volatile boolean aborted = false;
+ protected volatile Event latestEvent;
+
+ IntroductionResponse getResponse() {
+ assertTrue(
+ latestEvent instanceof IntroductionResponseReceivedEvent);
+ return ((IntroductionResponseReceivedEvent) latestEvent)
+ .getIntroductionResponse();
+ }
+ }
+
+ @MethodsNotNullByDefault
+ @ParametersNotNullByDefault
+ private class IntroduceeListener extends IntroductionListener {
private volatile boolean requestReceived = false;
private volatile boolean succeeded = false;
- private volatile boolean aborted = false;
private volatile boolean answerRequests = true;
private volatile SessionId sessionId;
@@ -1031,6 +1182,7 @@ public class IntroductionIntegrationTest
@Override
public void eventOccurred(Event e) {
if (e instanceof IntroductionRequestReceivedEvent) {
+ latestEvent = e;
IntroductionRequestReceivedEvent introEvent =
((IntroductionRequestReceivedEvent) e);
requestReceived = true;
@@ -1053,29 +1205,42 @@ public class IntroductionIntegrationTest
} finally {
eventWaiter.resume();
}
+ } else if (e instanceof IntroductionResponseReceivedEvent) {
+ // only broadcast for DECLINE messages in introducee role
+ latestEvent = e;
+ eventWaiter.resume();
} else if (e instanceof IntroductionSucceededEvent) {
+ latestEvent = e;
succeeded = true;
Contact contact = ((IntroductionSucceededEvent) e).getContact();
eventWaiter
.assertFalse(contact.getId().equals(contactId0From1));
eventWaiter.resume();
} else if (e instanceof IntroductionAbortedEvent) {
+ latestEvent = e;
aborted = true;
eventWaiter.resume();
}
}
+
+ private IntroductionRequest getRequest() {
+ assertTrue(
+ latestEvent instanceof IntroductionRequestReceivedEvent);
+ return ((IntroductionRequestReceivedEvent) latestEvent)
+ .getIntroductionRequest();
+ }
}
@NotNullByDefault
- private class IntroducerListener implements EventListener {
+ private class IntroducerListener extends IntroductionListener {
private volatile boolean response1Received = false;
private volatile boolean response2Received = false;
- private volatile boolean aborted = false;
@Override
public void eventOccurred(Event e) {
if (e instanceof IntroductionResponseReceivedEvent) {
+ latestEvent = e;
ContactId c =
((IntroductionResponseReceivedEvent) e)
.getContactId();
@@ -1086,6 +1251,7 @@ public class IntroductionIntegrationTest
}
eventWaiter.resume();
} else if (e instanceof IntroductionAbortedEvent) {
+ latestEvent = e;
aborted = true;
eventWaiter.resume();
}