From 6e42377b747e53d20f704f5ba154aa0a62e361ea Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Sat, 28 Apr 2018 00:11:45 +0100
Subject: [PATCH] Don't automatically respond to declined introduction.

---
 .../android/contact/ConversationActivity.java |  24 -----
 .../IntroduceeProtocolEngine.java             |  79 +++++++-------
 .../briar/introduction/IntroduceeState.java   |   9 +-
 .../IntroductionIntegrationTest.java          | 101 ++++--------------
 4 files changed, 71 insertions(+), 142 deletions(-)

diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
index 67572ff2bd..f926fe54d7 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
@@ -575,10 +575,6 @@ public class ConversationActivity extends BriarActivity
 			@Override
 			public void onSuccess(String contactName) {
 				runOnUiThreadUnlessDestroyed(() -> {
-					// If the other introducee declined, we can no longer
-					// respond to the request
-					if (!m.isIntroducer() && !m.wasAccepted())
-						markRequestAnswered(m.getSessionId());
 					ConversationItem item = ConversationItem
 							.from(ConversationActivity.this, contactName, m);
 					addConversationItem(item);
@@ -631,26 +627,6 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
-	private void markRequestAnswered(SessionId sessionId) {
-		int size = adapter.getItemCount();
-		for (int i = 0; i < size; i++) {
-			ConversationItem item = adapter.getItemAt(i);
-			if (item instanceof ConversationRequestItem) {
-				ConversationRequestItem req = (ConversationRequestItem) item;
-				if (req.getSessionId().equals(sessionId)
-						&& !req.wasAnswered()) {
-					LOG.info("Marking request answered");
-					req.setAnswered(true);
-					int position = adapter.findItemPosition(req);
-					if (position != INVALID_POSITION)
-						adapter.notifyItemChanged(position, req);
-					// There shouldn't be more than one unanswered request
-					return;
-				}
-			}
-		}
-	}
-
 	private void markMessages(Collection<MessageId> messageIds,
 			boolean sent, boolean seen) {
 		runOnUiThreadUnlessDestroyed(() -> {
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 e2ea32ed62..d93238dd8d 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
@@ -46,6 +46,7 @@ import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSE
 import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_ACCEPTED;
 import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED;
 import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_DECLINED;
 import static org.briarproject.briar.introduction.IntroduceeState.START;
 
 @Immutable
@@ -94,6 +95,7 @@ class IntroduceeProtocolEngine
 			IntroduceeSession session, long timestamp) throws DbException {
 		switch (session.getState()) {
 			case AWAIT_RESPONSES:
+			case REMOTE_DECLINED:
 			case REMOTE_ACCEPTED:
 				return onLocalAccept(txn, session, timestamp);
 			case START:
@@ -112,6 +114,7 @@ class IntroduceeProtocolEngine
 			IntroduceeSession session, long timestamp) throws DbException {
 		switch (session.getState()) {
 			case AWAIT_RESPONSES:
+			case REMOTE_DECLINED:
 			case REMOTE_ACCEPTED:
 				return onLocalDecline(txn, session, timestamp);
 			case START:
@@ -133,6 +136,7 @@ class IntroduceeProtocolEngine
 				return onRemoteRequest(txn, session, m);
 			case AWAIT_RESPONSES:
 			case LOCAL_DECLINED:
+			case REMOTE_DECLINED:
 			case LOCAL_ACCEPTED:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
@@ -153,6 +157,7 @@ class IntroduceeProtocolEngine
 			case LOCAL_ACCEPTED:
 				return onRemoteAccept(txn, session, m);
 			case START:
+			case REMOTE_DECLINED:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
 			case AWAIT_ACTIVATE:
@@ -172,6 +177,7 @@ class IntroduceeProtocolEngine
 			case LOCAL_ACCEPTED:
 				return onRemoteDecline(txn, session, m);
 			case START:
+			case REMOTE_DECLINED:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
 			case AWAIT_ACTIVATE:
@@ -190,6 +196,7 @@ class IntroduceeProtocolEngine
 			case START:
 			case AWAIT_RESPONSES:
 			case LOCAL_DECLINED:
+			case REMOTE_DECLINED:
 			case LOCAL_ACCEPTED:
 			case REMOTE_ACCEPTED:
 			case AWAIT_ACTIVATE:
@@ -208,6 +215,7 @@ class IntroduceeProtocolEngine
 			case START:
 			case AWAIT_RESPONSES:
 			case LOCAL_DECLINED:
+			case REMOTE_DECLINED:
 			case LOCAL_ACCEPTED:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
@@ -272,26 +280,28 @@ class IntroduceeProtocolEngine
 				transportPropertyManager.getLocalProperties(txn);
 
 		// Send a ACCEPT message
-		long localTimestamp =
-				Math.max(timestamp + 1, getLocalTimestamp(s));
+		long localTimestamp = Math.max(timestamp + 1, getLocalTimestamp(s));
 		Message sent = sendAcceptMessage(txn, s, localTimestamp, publicKey,
 				localTimestamp, transportProperties, true);
 		// Track the message
 		messageTracker.trackOutgoingMessage(txn, sent);
 
 		// Determine the next state
-		IntroduceeState state =
-				s.getState() == AWAIT_RESPONSES ? LOCAL_ACCEPTED : AWAIT_AUTH;
-		IntroduceeSession sNew = IntroduceeSession
-				.addLocalAccept(s, state, sent, publicKey, privateKey,
-						localTimestamp, transportProperties);
-
-		if (state == AWAIT_AUTH) {
-			// Move to the AWAIT_AUTH state
-			return onLocalAuth(txn, sNew);
+		switch (s.getState()) {
+			case AWAIT_RESPONSES:
+				return IntroduceeSession.addLocalAccept(s, LOCAL_ACCEPTED, sent,
+						publicKey, privateKey, localTimestamp,
+						transportProperties);
+			case REMOTE_DECLINED:
+				return IntroduceeSession.clear(s, START, sent.getId(),
+						localTimestamp, s.getLastRemoteMessageId());
+			case REMOTE_ACCEPTED:
+				return onLocalAuth(txn, IntroduceeSession.addLocalAccept(s,
+						AWAIT_AUTH, sent, publicKey, privateKey, localTimestamp,
+						transportProperties));
+			default:
+				throw new AssertionError();
 		}
-		// Move to the LOCAL_ACCEPTED state
-		return sNew;
 	}
 
 	private IntroduceeSession onLocalDecline(Transaction txn,
@@ -308,10 +318,9 @@ class IntroduceeProtocolEngine
 
 		// Move to the START or LOCAL_DECLINED state, if still awaiting response
 		IntroduceeState state =
-				s.getState() == REMOTE_ACCEPTED ? START : LOCAL_DECLINED;
-		return IntroduceeSession
-				.clear(s, state, sent.getId(), sent.getTimestamp(),
-						s.getLastRemoteMessageId());
+				s.getState() == AWAIT_RESPONSES ? LOCAL_DECLINED : START;
+		return IntroduceeSession.clear(s, state, sent.getId(),
+				sent.getTimestamp(), s.getLastRemoteMessageId());
 	}
 
 	private IntroduceeSession onRemoteAccept(Transaction txn,
@@ -357,22 +366,10 @@ class IntroduceeProtocolEngine
 		broadcastIntroductionResponseReceivedEvent(txn, s,
 				s.getIntroducer().getId(), s.getRemote().author, m);
 
-		if (s.getState() == AWAIT_RESPONSES) {
-			// Mark the request message unavailable to answer
-			markRequestsUnavailableToAnswer(txn, s);
-
-			// Send a DECLINE message
-			Message sent =
-					sendDeclineMessage(txn, s, getLocalTimestamp(s), false);
-
-			// Move back to START state
-			return IntroduceeSession.clear(s, START, sent.getId(),
-					sent.getTimestamp(), m.getMessageId());
-		} else {
-			// Move back to START state
-			return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
-					s.getLocalTimestamp(), m.getMessageId());
-		}
+		// Move to REMOTE_DECLINED state
+		return IntroduceeSession.clear(s, REMOTE_DECLINED,
+				s.getLastLocalMessageId(), s.getLocalTimestamp(),
+				m.getMessageId());
 	}
 
 	private IntroduceeSession onRemoteResponseWhenDeclined(Transaction txn,
@@ -385,6 +382,17 @@ class IntroduceeProtocolEngine
 		if (isInvalidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
 
+		// Mark the response visible in the UI
+		markMessageVisibleInUi(txn, m.getMessageId());
+
+		// Track the incoming message
+		messageTracker
+				.trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
+
+		// Broadcast IntroductionResponseReceivedEvent
+		broadcastIntroductionResponseReceivedEvent(txn, s,
+				s.getIntroducer().getId(), s.getRemote().author, m);
+
 		// Move to START state
 		return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
 				s.getLocalTimestamp(), m.getMessageId());
@@ -525,9 +533,8 @@ class IntroduceeProtocolEngine
 		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
 
 		// Reset the session back to initial state
-		return IntroduceeSession
-				.clear(s, START, sent.getId(), sent.getTimestamp(),
-						s.getLastRemoteMessageId());
+		return IntroduceeSession.clear(s, START, sent.getId(),
+				sent.getTimestamp(), s.getLastRemoteMessageId());
 	}
 
 	private boolean isInvalidDependency(IntroduceeSession s,
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java
index 7a54abde8d..3a3e16af36 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java
@@ -12,10 +12,11 @@ enum IntroduceeState implements State {
 	START(0),
 	AWAIT_RESPONSES(1),
 	LOCAL_DECLINED(2),
-	LOCAL_ACCEPTED(3),
-	REMOTE_ACCEPTED(4),
-	AWAIT_AUTH(5),
-	AWAIT_ACTIVATE(6);
+	REMOTE_DECLINED(3),
+	LOCAL_ACCEPTED(4),
+	REMOTE_ACCEPTED(5),
+	AWAIT_AUTH(6),
+	AWAIT_ACTIVATE(7);
 
 	private final int value;
 
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 b8e5b646bc..7441186e8b 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
@@ -17,7 +17,6 @@ import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
-import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.Message;
@@ -39,15 +38,12 @@ import org.junit.Test;
 
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.TimeoutException;
-import java.util.logging.Logger;
 
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getTransportId;
 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;
@@ -86,9 +82,6 @@ public class IntroductionIntegrationTest
 	private IntroduceeListener listener1;
 	private IntroduceeListener listener2;
 
-	private static final Logger LOG =
-			Logger.getLogger(IntroductionIntegrationTest.class.getName());
-
 	interface StateVisitor {
 		AcceptMessage visit(AcceptMessage response);
 	}
@@ -374,11 +367,10 @@ public class IntroductionIntegrationTest
 		assertEquals(2,
 				introductionManager0.getIntroductionMessages(contactId2From0)
 						.size());
-		// introducee1 also sees the decline response from introducee2
 		assertEquals(3,
 				introductionManager1.getIntroductionMessages(contactId0From1)
 						.size());
-		assertEquals(2,
+		assertEquals(3,
 				introductionManager2.getIntroductionMessages(contactId0From2)
 						.size());
 		assertFalse(listener0.aborted);
@@ -386,58 +378,6 @@ public class IntroductionIntegrationTest
 		assertFalse(listener2.aborted);
 	}
 
-	@Test
-	public void testIntroductionSessionDelayedFirstDecline() throws Exception {
-		addListeners(false, false);
-
-		// make introduction
-		long time = clock.currentTimeMillis();
-		introductionManager0
-				.makeIntroduction(contact1From0, contact2From0, null, time);
-
-		// sync request messages
-		sync0To1(1, true);
-		sync0To2(1, true);
-
-		// wait for requests to arrive
-		eventWaiter.await(TIMEOUT, 2);
-		assertTrue(listener1.requestReceived);
-		assertTrue(listener2.requestReceived);
-
-		// sync first response
-		sync1To0(1, true);
-		eventWaiter.await(TIMEOUT, 1);
-		assertTrue(listener0.response1Received);
-
-		// sync fake transport properties back to 1, so Message ACK can arrive
-		// and the assertDefaultUiMessages() check at the end will not fail
-		TransportProperties tp = new TransportProperties(
-				Collections.singletonMap("key", "value"));
-		c0.getTransportPropertyManager()
-				.mergeLocalProperties(getTransportId(), tp);
-		sync0To1(1, true);
-
-		// sync second response
-		sync2To0(1, true);
-		eventWaiter.await(TIMEOUT, 1);
-		assertTrue(listener0.response2Received);
-
-		// sync first forwarded response
-		sync0To2(1, true);
-
-		// note how the second response will not be forwarded anymore
-
-		assertFalse(contactManager1
-				.contactExists(author2.getId(), author1.getId()));
-		assertFalse(contactManager2
-				.contactExists(author1.getId(), author2.getId()));
-
-		assertDefaultUiMessages();
-		assertFalse(listener0.aborted);
-		assertFalse(listener1.aborted);
-		assertFalse(listener2.aborted);
-	}
-
 	@Test
 	public void testResponseAndAuthInOneSync() throws Exception {
 		addListeners(true, true);
@@ -467,9 +407,8 @@ public class IntroductionIntegrationTest
 		assertTrue(listener2.requestReceived);
 
 		// answer request manually
-		introductionManager2
-				.respondToIntroduction(contactId0From2, listener2.sessionId, time,
-						true);
+		introductionManager2.respondToIntroduction(contactId0From2,
+				listener2.sessionId, time, true);
 
 		// sync second response and AUTH
 		sync2To0(2, true);
@@ -479,7 +418,7 @@ public class IntroductionIntegrationTest
 		// Forward AUTH
 		sync0To1(1, true);
 
-		// Second AUTH and ACTIATE and forward them
+		// Second AUTH and ACTIVATE and forward them
 		sync1To0(2, true);
 		sync0To2(2, true);
 
@@ -495,13 +434,12 @@ public class IntroductionIntegrationTest
 	}
 
 	/**
-	 * 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
+	 * When an introducee declines an introduction, the other introducee needs
+	 * to respond before returning to the 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
+	public void testIntroductionSessionManualDecline() throws Exception {
 		addListeners(false, true);
 		listener2.answerRequests = false;
 
@@ -547,9 +485,18 @@ public class IntroductionIntegrationTest
 
 		// assert that introducee2 is in correct state
 		introduceeSession = getIntroduceeSession(c2);
+		assertEquals(IntroduceeState.REMOTE_DECLINED,
+				introduceeSession.getState());
+
+		// answer request manually
+		introductionManager2.respondToIntroduction(contactId0From2,
+				listener2.sessionId, time, false);
+
+		// now introducee2 should have returned to the START state
+		introduceeSession = getIntroduceeSession(c2);
 		assertEquals(IntroduceeState.START, introduceeSession.getState());
 
-		// second response should be an immediate automatic DECLINE
+		// sync second response
 		sync2To0(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.response2Received);
@@ -562,7 +509,7 @@ public class IntroductionIntegrationTest
 		introduceeSession = getIntroduceeSession(c1);
 		assertEquals(LOCAL_DECLINED, introduceeSession.getState());
 
-		// forward automatic decline
+		// forward second response
 		sync0To1(1, true);
 
 		// introducee1 can finally move to the START
@@ -579,16 +526,14 @@ public class IntroductionIntegrationTest
 				introductionManager0.getIntroductionMessages(contactId2From0)
 						.size());
 		assertGroupCount(messageTracker0, g2.getId(), 2, 1);
-		assertEquals(2,
+		assertEquals(3,
 				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,
+		assertGroupCount(messageTracker1, g1.getId(), 3, 2);
+		assertEquals(3,
 				introductionManager2.getIntroductionMessages(contactId0From2)
 						.size());
-		assertGroupCount(messageTracker2, g2.getId(), 2, 2);
+		assertGroupCount(messageTracker2, g2.getId(), 3, 2);
 
 		assertFalse(listener0.aborted);
 		assertFalse(listener1.aborted);
-- 
GitLab