diff --git a/briar-android-tests/build.gradle b/briar-android-tests/build.gradle
index 8c329bb6e3aabecbcacd215980d3075e440033e7..ba1dd6ec21eb2e86e35dcbd9b08a6af61eae5473 100644
--- a/briar-android-tests/build.gradle
+++ b/briar-android-tests/build.gradle
@@ -31,6 +31,7 @@ dependencies {
     compile project(':briar-api')
     compile project(':briar-core')
     testCompile 'junit:junit:4.12'
+    testCompile 'net.jodah:concurrentunit:0.4.2'
     compile 'com.android.support:appcompat-v7:23.2.0'
     testApt 'com.google.dagger:dagger-compiler:2.0.2'
     provided 'javax.annotation:jsr250-api:1.0'
diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f2f02be254eda522d1850d2cc47f1c5c6248f09
--- /dev/null
+++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java
@@ -0,0 +1,750 @@
+package org.briarproject;
+
+import net.jodah.concurrentunit.Waiter;
+
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.IntroductionAbortedEvent;
+import org.briarproject.api.event.IntroductionRequestReceivedEvent;
+import org.briarproject.api.event.IntroductionResponseReceivedEvent;
+import org.briarproject.api.event.IntroductionSucceededEvent;
+import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.introduction.SessionId;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.properties.TransportProperties;
+import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.sync.SyncSession;
+import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.api.system.Clock;
+import org.briarproject.contact.ContactModule;
+import org.briarproject.crypto.CryptoModule;
+import org.briarproject.introduction.IntroductionModule;
+import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.properties.PropertiesModule;
+import org.briarproject.sync.SyncModule;
+import org.briarproject.transport.TransportModule;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+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.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class IntroductionIntegrationTest extends BriarTestCase {
+
+	LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2;
+	SyncSessionFactory sync0, sync1, sync2;
+	ContactManager contactManager0, contactManager1, contactManager2;
+	ContactId contactId0, contactId1, contactId2;
+	IdentityManager identityManager0, identityManager1, identityManager2;
+	LocalAuthor author0, author1, author2;
+
+	@Inject
+	Clock clock;
+	@Inject
+	AuthorFactory authorFactory;
+
+	// objects accessed from background threads need to be volatile
+	private volatile IntroductionManager introductionManager0;
+	private volatile IntroductionManager introductionManager1;
+	private volatile IntroductionManager introductionManager2;
+	private volatile Waiter eventWaiter;
+	private volatile Waiter msgWaiter;
+
+	private final File testDir = TestUtils.getTestDirectory();
+	private final SecretKey master = TestUtils.getSecretKey();
+	private final int TIMEOUT = 15000;
+	private final String INTRODUCER = "Introducer";
+	private final String INTRODUCEE1 = "Introducee1";
+	private final String INTRODUCEE2 = "Introducee2";
+
+	private static final Logger LOG =
+			Logger.getLogger(IntroductionIntegrationTest.class.getName());
+
+	private IntroductionIntegrationTestComponent t0, t1, t2;
+
+	@Before
+	public void setUp() {
+		IntroductionIntegrationTestComponent component =
+				DaggerIntroductionIntegrationTestComponent.builder().build();
+		component.inject(this);
+		injectEagerSingletons(component);
+
+		assertTrue(testDir.mkdirs());
+		File t0Dir = new File(testDir, INTRODUCER);
+		t0 = DaggerIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
+		injectEagerSingletons(t0);
+		File t1Dir = new File(testDir, INTRODUCEE1);
+		t1 = DaggerIntroductionIntegrationTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
+		injectEagerSingletons(t1);
+		File t2Dir = new File(testDir, INTRODUCEE2);
+		t2 = DaggerIntroductionIntegrationTestComponent.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();
+		introductionManager0 = t0.getIntroductionManager();
+		introductionManager1 = t1.getIntroductionManager();
+		introductionManager2 = t2.getIntroductionManager();
+		sync0 = t0.getSyncSessionFactory();
+		sync1 = t1.getSyncSessionFactory();
+		sync2 = t2.getSyncSessionFactory();
+
+		// initialize waiters fresh for each test
+		eventWaiter = new Waiter();
+		msgWaiter = new Waiter();
+	}
+
+	@Test
+	public void testIntroductionSession() 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(), true,
+					true
+			);
+			ContactId contactId02 = contactManager2.addContact(author0,
+					author2.getId(), master, clock.currentTimeMillis(), true,
+					true
+			);
+			assertTrue(contactId0.equals(contactId02));
+
+			// listen to events
+			IntroducerListener listener0 = new IntroducerListener();
+			t0.getEventBus().addListener(listener0);
+			IntroduceeListener listener1 = new IntroduceeListener(1, true);
+			t1.getEventBus().addListener(listener1);
+			IntroduceeListener listener2 = new IntroduceeListener(2, true);
+			t2.getEventBus().addListener(listener2);
+
+			// make introduction
+			long time = clock.currentTimeMillis();
+			Contact introducee1 = contactManager0.getContact(contactId1);
+			Contact introducee2 = contactManager0.getContact(contactId2);
+			introductionManager0
+					.makeIntroduction(introducee1, introducee2, "Hi!", time);
+
+			// sync first request message
+			deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
+			eventWaiter.await(TIMEOUT, 1);
+			assertTrue(listener1.requestReceived);
+
+			// sync second request message
+			deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
+			eventWaiter.await(TIMEOUT, 1);
+			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 forwarded responses to introducees
+			deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
+			deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
+
+			// sync first ACK and its forward
+			deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
+			deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
+
+			// sync second ACK and its forward
+			deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0");
+			deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 2");
+
+			// wait for introduction to succeed
+			eventWaiter.await(TIMEOUT, 2);
+			assertTrue(listener1.succeeded);
+			assertTrue(listener2.succeeded);
+
+			assertTrue(contactManager1
+					.contactExists(author2.getId(), author1.getId()));
+			assertTrue(contactManager2
+					.contactExists(author1.getId(), author2.getId()));
+
+			assertDefaultUiMessages();
+		} finally {
+			stopLifecycles();
+		}
+	}
+
+	@Test
+	public void testIntroductionSessionFirstDecline() 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(), true, true
+			);
+			ContactId contactId02 = contactManager2.addContact(author0,
+					author2.getId(), master, clock.currentTimeMillis(), true,
+					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, true);
+			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 introducer does not forward the second response,
+			// because after the first decline the protocol finished
+
+			assertFalse(listener1.succeeded);
+			assertFalse(listener2.succeeded);
+
+			assertFalse(contactManager1
+					.contactExists(author2.getId(), author1.getId()));
+			assertFalse(contactManager2
+					.contactExists(author1.getId(), author2.getId()));
+
+			assertDefaultUiMessages();
+		} finally {
+			stopLifecycles();
+		}
+	}
+
+	@Test
+	public void testIntroductionSessionSecondDecline() 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, true);
+			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 both forwarded response
+			deliverMessage(sync0, contactId0, sync2, contactId2);
+			deliverMessage(sync0, contactId0, sync1, contactId1);
+
+			assertFalse(contactManager1
+					.contactExists(author2.getId(), author1.getId()));
+			assertFalse(contactManager2
+					.contactExists(author1.getId(), author2.getId()));
+
+			assertDefaultUiMessages();
+		} finally {
+			stopLifecycles();
+		}
+	}
+
+	@Test
+	public void testIntroductionToSameContact() throws Exception {
+		startLifecycles();
+		try {
+			// Add Identities
+			addDefaultIdentities();
+
+			// Add Transport Properties
+			addTransportProperties();
+
+			// Add introducee as contact
+			contactId1 = contactManager0.addContact(author1, author0.getId(),
+					master, clock.currentTimeMillis(), true, true
+			);
+			// Add introducer back
+			contactId0 = contactManager1.addContact(author0, author1.getId(),
+					master, clock.currentTimeMillis(), true, true
+			);
+
+			// listen to events
+			IntroducerListener listener0 = new IntroducerListener();
+			t0.getEventBus().addListener(listener0);
+			IntroduceeListener listener1 = new IntroduceeListener(1, true);
+			t1.getEventBus().addListener(listener1);
+
+			// make introduction
+			long time = clock.currentTimeMillis();
+			Contact introducee1 = contactManager0.getContact(contactId1);
+			introductionManager0
+					.makeIntroduction(introducee1, introducee1, null, time);
+
+			// sync request messages
+			deliverMessage(sync0, contactId0, sync1, contactId1);
+
+			// we should not get any event, because the request will be discarded
+			assertFalse(listener1.requestReceived);
+
+			// make really sure we don't have that request
+			assertTrue(introductionManager1.getIntroductionMessages(contactId0)
+					.isEmpty());
+		} finally {
+			stopLifecycles();
+		}
+	}
+
+	@Test
+	public void testIntroductionToIdentitiesOfSameContact() throws Exception {
+		startLifecycles();
+		try {
+			// Add Identities
+			author0 = authorFactory.createLocalAuthor(INTRODUCER,
+					TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+					TestUtils.getRandomBytes(123));
+			identityManager0.addLocalAuthor(author0);
+			author1 = authorFactory.createLocalAuthor(INTRODUCEE1,
+					TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+					TestUtils.getRandomBytes(123));
+			identityManager1.addLocalAuthor(author1);
+			author2 = authorFactory.createLocalAuthor(INTRODUCEE2,
+					TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+					TestUtils.getRandomBytes(123));
+			identityManager1.addLocalAuthor(author2);
+
+			// Add Transport Properties
+			addTransportProperties();
+
+			// Add introducees' authors 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 = null;
+			ContactId contactId01 = contactManager1.addContact(author0,
+					author1.getId(), master, clock.currentTimeMillis(), false,
+					true
+			);
+			ContactId contactId02 = contactManager1.addContact(author0,
+					author2.getId(), master, clock.currentTimeMillis(), false,
+					true
+			);
+
+			// listen to events
+			IntroducerListener listener0 = new IntroducerListener();
+			t0.getEventBus().addListener(listener0);
+			IntroduceeListener listener1 = new IntroduceeListener(1, true);
+			t1.getEventBus().addListener(listener1);
+
+			// make introduction
+			long time = clock.currentTimeMillis();
+			Contact introducee1 = contactManager0.getContact(contactId1);
+			Contact introducee2 = contactManager0.getContact(contactId2);
+			introductionManager0
+					.makeIntroduction(introducee1, introducee2, "Hi!", time);
+
+			// sync request messages
+			deliverMessage(sync0, contactId01, sync1, contactId1);
+			deliverMessage(sync0, contactId02, sync1, contactId2);
+
+			// wait for request to arrive
+			eventWaiter.await(TIMEOUT, 2);
+			assertTrue(listener1.requestReceived);
+
+			// sync responses
+			deliverMessage(sync1, contactId1, sync0, contactId01);
+			deliverMessage(sync1, contactId2, sync0, contactId02);
+
+			// wait for two responses to arrive
+			eventWaiter.await(TIMEOUT, 2);
+			assertTrue(listener0.response1Received);
+			assertTrue(listener0.response2Received);
+
+			// sync forwarded responses to introducees
+			deliverMessage(sync0, contactId01, sync1, contactId1);
+			deliverMessage(sync0, contactId02, sync1, contactId2);
+
+			// wait for "both" introducees to abort session
+			eventWaiter.await(TIMEOUT, 2);
+			assertTrue(listener1.aborted);
+
+			// sync abort message
+			deliverMessage(sync1, contactId1, sync0, contactId01);
+			deliverMessage(sync1, contactId2, sync0, contactId02);
+
+			// wait for introducer to abort session (gets event twice)
+			eventWaiter.await(TIMEOUT, 2);
+			assertTrue(listener0.aborted);
+
+			assertFalse(contactManager1
+					.contactExists(author1.getId(), author2.getId()));
+			assertFalse(contactManager1
+					.contactExists(author2.getId(), author1.getId()));
+
+			assertTrue(introductionManager0.getIntroductionMessages(contactId1)
+					.size() == 2);
+			assertTrue(introductionManager0.getIntroductionMessages(contactId2)
+					.size() == 2);
+			assertTrue(introductionManager1.getIntroductionMessages(contactId01)
+					.size() == 2);
+			assertTrue(introductionManager1.getIntroductionMessages(contactId02)
+					.size() == 2);
+		} finally {
+			stopLifecycles();
+		}
+	}
+
+	// TODO add a test for faking responses when #256 is implemented
+
+	@After
+	public void tearDown() throws InterruptedException {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+
+	private void startLifecycles() throws InterruptedException {
+		// 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 addTransportProperties() throws DbException {
+		TransportPropertyManager tpm0 = t0.getTransportPropertyManager();
+		TransportPropertyManager tpm1 = t1.getTransportPropertyManager();
+		TransportPropertyManager tpm2 = t2.getTransportPropertyManager();
+
+		TransportProperties tp = new TransportProperties(
+				Collections.singletonMap("key", "value"));
+		tpm0.mergeLocalProperties(TRANSPORT_ID, tp);
+		tpm1.mergeLocalProperties(TRANSPORT_ID, tp);
+		tpm2.mergeLocalProperties(TRANSPORT_ID, tp);
+	}
+
+	private void addDefaultIdentities() throws DbException {
+		author0 = authorFactory.createLocalAuthor(INTRODUCER,
+				TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				TestUtils.getRandomBytes(123));
+		identityManager0.addLocalAuthor(author0);
+		author1 = authorFactory.createLocalAuthor(INTRODUCEE1,
+				TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				TestUtils.getRandomBytes(123));
+		identityManager1.addLocalAuthor(author1);
+		author2 = authorFactory.createLocalAuthor(INTRODUCEE2,
+				TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				TestUtils.getRandomBytes(123));
+		identityManager2.addLocalAuthor(author2);
+	}
+
+	private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
+			SyncSessionFactory toSync, ContactId toId)
+			throws IOException, TimeoutException {
+		deliverMessage(fromSync, fromId, toSync, toId, null);
+	}
+
+	private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
+			SyncSessionFactory toSync, ContactId toId, String debug)
+			throws IOException, TimeoutException {
+
+		if (debug != null) LOG.info("TEST: Sending message from " + debug);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		// Create an outgoing sync session
+		SyncSession sessionFrom =
+				fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
+		// Write whatever needs to be written
+		sessionFrom.run();
+		out.close();
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		// Create an incoming sync session
+		SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
+		// Read whatever needs to be read
+		sessionTo.run();
+		in.close();
+
+		// wait for message to actually arrive
+		msgWaiter.await(TIMEOUT, 1);
+	}
+
+	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);
+	}
+
+	private class IntroduceeListener implements EventListener {
+
+		public volatile boolean requestReceived = false;
+		public volatile boolean succeeded = false;
+		public volatile boolean aborted = false;
+
+		private final int introducee;
+		private final boolean accept;
+
+		IntroduceeListener(int introducee, boolean accept) {
+			this.introducee = introducee;
+			this.accept = accept;
+		}
+
+		public void eventOccurred(Event e) {
+			if (e instanceof MessageValidatedEvent) {
+				MessageValidatedEvent event = (MessageValidatedEvent) e;
+				if (event.getClientId()
+						.equals(introductionManager0.getClientId()) &&
+						!event.isLocal()) {
+					LOG.info("TEST: Introducee" + introducee +
+							" received message in group " +
+							((MessageValidatedEvent) e).getMessage()
+									.getGroupId().hashCode());
+					msgWaiter.resume();
+				}
+			} else if (e instanceof IntroductionRequestReceivedEvent) {
+				IntroductionRequestReceivedEvent introEvent =
+						((IntroductionRequestReceivedEvent) e);
+				requestReceived = true;
+				IntroductionRequest ir = introEvent.getIntroductionRequest();
+				ContactId contactId = introEvent.getContactId();
+				SessionId sessionId = ir.getSessionId();
+				long time = clock.currentTimeMillis();
+				try {
+					if (introducee == 1) {
+						if (accept) {
+							introductionManager1
+									.acceptIntroduction(contactId, sessionId,
+											time);
+						} else {
+							introductionManager1
+									.declineIntroduction(contactId, sessionId,
+											time);
+						}
+					} else if (introducee == 2) {
+						if (accept) {
+							introductionManager2
+									.acceptIntroduction(contactId, sessionId,
+											time);
+						} else {
+							introductionManager2
+									.declineIntroduction(contactId, sessionId,
+											time);
+						}
+					}
+				} catch (DbException | IOException exception) {
+					eventWaiter.rethrow(exception);
+				} finally {
+					eventWaiter.resume();
+				}
+			} else if (e instanceof IntroductionSucceededEvent) {
+				succeeded = true;
+				Contact contact = ((IntroductionSucceededEvent) e).getContact();
+				eventWaiter.assertFalse(contact.getId().equals(contactId0));
+				eventWaiter.assertTrue(contact.isActive());
+				eventWaiter.resume();
+			} else if (e instanceof IntroductionAbortedEvent) {
+				aborted = true;
+				eventWaiter.resume();
+			}
+		}
+	}
+
+	private class IntroducerListener implements EventListener {
+
+		public volatile boolean response1Received = false;
+		public volatile boolean response2Received = false;
+		public volatile boolean aborted = false;
+
+		public void eventOccurred(Event e) {
+			if (e instanceof MessageValidatedEvent) {
+				MessageValidatedEvent event = (MessageValidatedEvent) e;
+				if (event.getClientId()
+						.equals(introductionManager0.getClientId()) &&
+						!event.isLocal()) {
+					LOG.info("TEST: Introducer received message in group " +
+							((MessageValidatedEvent) e).getMessage()
+									.getGroupId().hashCode());
+					msgWaiter.resume();
+				}
+			} else if (e instanceof IntroductionResponseReceivedEvent) {
+				ContactId c =
+						((IntroductionResponseReceivedEvent) e).getContactId();
+				try {
+					if (c.equals(contactId1)) {
+						response1Received = true;
+					} else if (c.equals(contactId2)) {
+						response2Received = true;
+					}
+				} finally {
+					eventWaiter.resume();
+				}
+			} else if (e instanceof IntroductionAbortedEvent) {
+				aborted = true;
+				eventWaiter.resume();
+			}
+		}
+	}
+
+	private void injectEagerSingletons(
+			IntroductionIntegrationTestComponent component) {
+
+		component.inject(new LifecycleModule.EagerSingletons());
+		component.inject(new LifecycleModule.EagerSingletons());
+		component.inject(new IntroductionModule.EagerSingletons());
+		component.inject(new CryptoModule.EagerSingletons());
+		component.inject(new ContactModule.EagerSingletons());
+		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new SyncModule.EagerSingletons());
+		component.inject(new PropertiesModule.EagerSingletons());
+	}
+
+}
diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTestComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..b027327565cce75a422788738a3442ab6b21b5fa
--- /dev/null
+++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTestComponent.java
@@ -0,0 +1,77 @@
+package org.briarproject;
+
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.clients.ClientsModule;
+import org.briarproject.contact.ContactModule;
+import org.briarproject.crypto.CryptoModule;
+import org.briarproject.data.DataModule;
+import org.briarproject.db.DatabaseModule;
+import org.briarproject.event.EventModule;
+import org.briarproject.identity.IdentityModule;
+import org.briarproject.introduction.IntroductionModule;
+import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.properties.PropertiesModule;
+import org.briarproject.sync.SyncModule;
+import org.briarproject.transport.TransportModule;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+@Singleton
+@Component(modules = {
+		TestSystemModule.class,
+		TestDatabaseModule.class,
+		TestPluginsModule.class,
+		LifecycleModule.class,
+		IntroductionModule.class,
+		DatabaseModule.class,
+		CryptoModule.class,
+		EventModule.class,
+		ContactModule.class,
+		IdentityModule.class,
+		TransportModule.class,
+		ClientsModule.class,
+		SyncModule.class,
+		DataModule.class,
+		PropertiesModule.class
+})
+public interface IntroductionIntegrationTestComponent {
+
+	void inject(IntroductionIntegrationTest testCase);
+
+	void inject(ContactModule.EagerSingletons init);
+
+	void inject(CryptoModule.EagerSingletons init);
+
+	void inject(IntroductionModule.EagerSingletons init);
+
+	void inject(LifecycleModule.EagerSingletons init);
+
+	void inject(PropertiesModule.EagerSingletons init);
+
+	void inject(SyncModule.EagerSingletons init);
+
+	void inject(TransportModule.EagerSingletons init);
+
+	LifecycleManager getLifecycleManager();
+
+	EventBus getEventBus();
+
+	IdentityManager getIdentityManager();
+
+	ContactManager getContactManager();
+
+	IntroductionManager getIntroductionManager();
+
+	TransportPropertyManager getTransportPropertyManager();
+
+	SyncSessionFactory getSyncSessionFactory();
+
+}
diff --git a/briar-api/src/org/briarproject/api/event/IntroductionAbortedEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionAbortedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..258d588e284a05c980aa7c193ff73f8993fe7305
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/IntroductionAbortedEvent.java
@@ -0,0 +1,23 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.introduction.SessionId;
+
+public class IntroductionAbortedEvent extends Event {
+
+	private final ContactId contactId;
+	private final SessionId sessionId;
+
+	public IntroductionAbortedEvent(ContactId contactId, SessionId sessionId) {
+		this.contactId = contactId;
+		this.sessionId = sessionId;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
index e713fb8bd8e3ea2a81a46e6ff1d632da5db53e6d..ad6e005f6e69426056e8cdac760f4bc1746ecd25 100644
--- a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
+++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
@@ -5,6 +5,7 @@ import org.briarproject.api.ProtocolEngine;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.event.Event;
+import org.briarproject.api.event.IntroductionAbortedEvent;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.introduction.IntroduceeAction;
@@ -25,6 +26,8 @@ import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ABORT;
 import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ACCEPT;
 import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_DECLINE;
 import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ABORT;
+import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ACCEPT;
+import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_DECLINE;
 import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_ACK;
 import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REMOTE_RESPONSE;
 import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
@@ -92,7 +95,7 @@ public class IntroduceeEngine
 							currentState.name());
 				}
 				if (currentState == ERROR) return noUpdate(localState);
-				else abortSession(currentState, localState);
+				else return abortSession(currentState, localState);
 			}
 
 			if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) {
@@ -194,6 +197,11 @@ public class IntroduceeEngine
 			}
 			// we are done (probably declined response) and ignore this 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);
 			}
 			// this should not happen
@@ -355,8 +363,14 @@ public class IntroduceeEngine
 		msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
 		msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
 		List<BdfDictionary> messages = Collections.singletonList(msg);
-		// TODO inform about protocol abort via new Event?
-		List<Event> events = Collections.emptyList();
+
+		// send abort event
+		ContactId contactId =
+				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
+		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
+		Event event = new IntroductionAbortedEvent(contactId, sessionId);
+		List<Event> events = Collections.singletonList(event);
+
 		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
 				localState, messages, events);
 	}
diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
index 796d528f1d09b5fb9b49963fef8a732efbcb25de..6beacb934456e2fecda959f7e1c627a7baa4ea93 100644
--- a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
+++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
@@ -5,6 +5,7 @@ import org.briarproject.api.ProtocolEngine;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.event.Event;
+import org.briarproject.api.event.IntroductionAbortedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.introduction.IntroducerAction;
@@ -56,7 +57,6 @@ 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.SESSION_ID;
 import static org.briarproject.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.api.introduction.IntroductionConstants.TIME;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
@@ -288,11 +288,11 @@ public class IntroducerEngine
 
 		ContactId contactId =
 				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
-		AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1, new byte[32])); // TODO remove byte[]
+		AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1));
 		if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
 			contactId =
 					new ContactId(localState.getLong(CONTACT_ID_2).intValue());
-			authorId = new AuthorId(localState.getRaw(AUTHOR_ID_2, new byte[32])); // TODO remove byte[]
+			authorId = new AuthorId(localState.getRaw(AUTHOR_ID_2));
 		}
 
 		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
@@ -365,8 +365,19 @@ public class IntroducerEngine
 		msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
 		msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
 		messages.add(msg2);
-		// TODO inform about protocol abort via new Event?
-		List<Event> events = Collections.emptyList();
+
+		// send one abort event per contact
+		List<Event> events = new ArrayList<Event>(2);
+		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
+		ContactId contactId1 =
+				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
+		ContactId contactId2 =
+				new ContactId(localState.getLong(CONTACT_ID_2).intValue());
+		Event event1 = new IntroductionAbortedEvent(contactId1, sessionId);
+		events.add(event1);
+		Event event2 = new IntroductionAbortedEvent(contactId2, sessionId);
+		events.add(event2);
+
 		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
 				localState, messages, events);
 	}
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionModule.java b/briar-core/src/org/briarproject/introduction/IntroductionModule.java
index 633d14c15bbf8c579b259eca808a19b65ee7bee1..9e51aca734a7bec5c1317e413593cf2285861b45 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionModule.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionModule.java
@@ -3,13 +3,9 @@ package org.briarproject.introduction;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.contact.ContactManager;
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.introduction.IntroductionManager;
 import org.briarproject.api.lifecycle.LifecycleManager;
-import org.briarproject.api.properties.TransportPropertyManager;
 import org.briarproject.api.system.Clock;
 
 import javax.inject.Inject;
@@ -18,17 +14,19 @@ import javax.inject.Singleton;
 import dagger.Module;
 import dagger.Provides;
 
+import static org.briarproject.api.sync.ValidationManager.MessageValidator;
+
 @Module
 public class IntroductionModule {
 
 	public static class EagerSingletons {
 		@Inject IntroductionManager introductionManager;
-		@Inject IntroductionValidator introductionValidator;
+		@Inject MessageValidator introductionValidator;
 	}
 
 	@Provides
 	@Singleton
-	IntroductionValidator getValidator(MessageQueueManager messageQueueManager,
+	MessageValidator getValidator(MessageQueueManager messageQueueManager,
 			IntroductionManager introductionManager,
 			MetadataEncoder metadataEncoder, ClientHelper clientHelper,
 			Clock clock) {