diff --git a/briar-core/src/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java b/briar-core/src/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
index 1581db927252a9670f6a3d97bb58eda189b01331..4036e10d871daaecd195be6f54d0c39cc8071ba4 100644
--- a/briar-core/src/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
+++ b/briar-core/src/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
@@ -29,6 +29,7 @@ import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.duplex.DuplexPlugin;
 import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
+import net.sf.briar.util.LatchedReference;
 import net.sf.briar.util.OsUtils;
 import net.sf.briar.util.StringUtils;
 
@@ -112,20 +113,21 @@ class BluetoothPlugin implements DuplexPlugin {
 		TransportProperties p = new TransportProperties();
 		p.put("address", localDevice.getBluetoothAddress());
 		callback.mergeLocalProperties(p);
+		// Bind a server socket to accept connections from contacts
 		String url = makeUrl("localhost", getUuid());
-		StreamConnectionNotifier scn;
+		StreamConnectionNotifier ss;
 		try {
-			scn = (StreamConnectionNotifier) Connector.open(url);
+			ss = (StreamConnectionNotifier) Connector.open(url);
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			return;
 		}
 		if(!running) {
-			tryToClose(scn);
+			tryToClose(ss);
 			return;
 		}
-		socket = scn;
-		acceptContactConnections(scn);
+		socket = ss;
+		acceptContactConnections(ss);
 	}
 
 	private String makeUrl(String address, String uuid) {
@@ -145,23 +147,23 @@ class BluetoothPlugin implements DuplexPlugin {
 		return uuid;
 	}
 
-	private void tryToClose(StreamConnectionNotifier scn) {
+	private void tryToClose(StreamConnectionNotifier ss) {
 		try {
-			scn.close();
+			if(ss != null) ss.close();
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
 	}
 
-	private void acceptContactConnections(StreamConnectionNotifier scn) {
+	private void acceptContactConnections(StreamConnectionNotifier ss) {
 		while(true) {
 			StreamConnection s;
 			try {
-				s = scn.acceptAndOpen();
+				s = ss.acceptAndOpen();
 			} catch(IOException e) {
 				// This is expected when the socket is closed
 				if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
-				tryToClose(scn);
+				tryToClose(ss);
 				return;
 			}
 			BluetoothTransportConnection conn =
@@ -173,7 +175,7 @@ class BluetoothPlugin implements DuplexPlugin {
 
 	public void stop() {
 		running = false;
-		if(socket != null) tryToClose(socket);
+		tryToClose(socket);
 	}
 
 	public boolean shouldPoll() {
@@ -199,19 +201,20 @@ class BluetoothPlugin implements DuplexPlugin {
 			pluginExecutor.execute(new Runnable() {
 				public void run() {
 					if(!running) return;
-					String url = makeUrl(address, uuid);
-					DuplexTransportConnection conn = connect(url);
-					if(conn != null)
-						callback.outgoingConnectionCreated(c, conn);
+					StreamConnection s = connect(makeUrl(address, uuid));
+					if(s != null) {
+						callback.outgoingConnectionCreated(c,
+								new BluetoothTransportConnection(
+										BluetoothPlugin.this, s));
+					}
 				}
 			});
 		}
 	}
 
-	private DuplexTransportConnection connect(String url) {
+	private StreamConnection connect(String url) {
 		try {
-			StreamConnection s = (StreamConnection) Connector.open(url);
-			return new BluetoothTransportConnection(this, s);
+			return (StreamConnection) Connector.open(url);
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			return null;
@@ -227,7 +230,9 @@ class BluetoothPlugin implements DuplexPlugin {
 		String uuid = p.get("uuid");
 		if(StringUtils.isNullOrEmpty(uuid)) return null;
 		String url = makeUrl(address, uuid);
-		return connect(url);
+		StreamConnection s = connect(url);
+		if(s == null) return null;
+		return new BluetoothTransportConnection(this, s);
 	}
 
 	public boolean supportsInvitations() {
@@ -237,84 +242,42 @@ class BluetoothPlugin implements DuplexPlugin {
 	public DuplexTransportConnection sendInvitation(PseudoRandom r,
 			long timeout) {
 		if(!running) return null;
-		// Use the same pseudo-random UUID as the contact
-		byte[] b = r.nextBytes(UUID_BYTES);
-		String uuid = UUID.nameUUIDFromBytes(b).toString();
-		// Discover nearby devices and connect to any with the right UUID
-		DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
-		long end = clock.currentTimeMillis() + timeout;
-		String url = null;
-		while(url == null && clock.currentTimeMillis() < end) {
-			if(!discoverySemaphore.tryAcquire()) {
-				if(LOG.isLoggable(INFO))
-					LOG.info("Another device discovery is in progress");
-				return null;
-			}
-			try {
-				InvitationListener listener =
-						new InvitationListener(discoveryAgent, uuid);
-				discoveryAgent.startInquiry(GIAC, listener);
-				url = listener.waitForUrl();
-			} catch(BluetoothStateException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				return null;
-			} catch(InterruptedException e) {
-				if(LOG.isLoggable(INFO))
-					LOG.info("Interrupted while waiting for URL");
-				Thread.currentThread().interrupt();
-				return null;
-			} finally {
-				discoverySemaphore.release();
-			}
-			if(!running) return null;
-		}
-		if(url == null) return null;
-		return connect(url);
-	}
-
-	public DuplexTransportConnection acceptInvitation(PseudoRandom r,
-			final long timeout) {
-		if(!running) return null;
-		// Use the same pseudo-random UUID as the contact
+		// Use the invitation codes to generate the UUID
 		byte[] b = r.nextBytes(UUID_BYTES);
 		String uuid = UUID.nameUUIDFromBytes(b).toString();
 		String url = makeUrl("localhost", uuid);
 		// Make the device discoverable if possible
 		makeDeviceDiscoverable();
-		// Bind a socket for accepting the invitation connection
-		final StreamConnectionNotifier scn;
+		// Bind a server socket for receiving invitation connections
+		final StreamConnectionNotifier ss;
 		try {
-			scn = (StreamConnectionNotifier) Connector.open(url);
+			ss = (StreamConnectionNotifier) Connector.open(url);
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			return null;
 		}
 		if(!running) {
-			tryToClose(scn);
+			tryToClose(ss);
 			return null;
 		}
-		// Close the socket when the invitation times out
-		new Thread() {
-			@Override
-			public void run() {
-				try {
-					Thread.sleep(timeout);
-				} catch(InterruptedException e) {
-					if(LOG.isLoggable(WARNING))
-						LOG.warning("Interrupted while waiting for invitation");
-				}
-				tryToClose(scn);
-			}
-		}.start();
-		// Try to accept a connection
+		// Start the background threads
+		LatchedReference<StreamConnection> socketLatch =
+				new LatchedReference<StreamConnection>();
+		new DiscoveryThread(socketLatch, uuid, timeout).start();
+		new BluetoothListenerThread(socketLatch, ss).start();
+		// Wait for an incoming or outgoing connection
 		try {
-			StreamConnection s = scn.acceptAndOpen();
-			return new BluetoothTransportConnection(this, s);
-		} catch(IOException e) {
-			// This is expected when the socket is closed
-			if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
-			return null;
+			StreamConnection s = socketLatch.waitForReference(timeout);
+			if(s != null) return new BluetoothTransportConnection(this, s);
+		} catch(InterruptedException e) {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Interrupted while exchanging invitations");
+			Thread.currentThread().interrupt();
+		} finally {
+			// Closing the socket will terminate the listener thread
+			tryToClose(ss);
 		}
+		return null;
 	}
 
 	private void makeDeviceDiscoverable() {
@@ -326,4 +289,107 @@ class BluetoothPlugin implements DuplexPlugin {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
 	}
+
+	public DuplexTransportConnection acceptInvitation(PseudoRandom r,
+			long timeout) {
+		// FIXME
+		return sendInvitation(r, timeout);
+	}
+
+	private class DiscoveryThread extends Thread {
+
+		private final LatchedReference<StreamConnection> socketLatch;
+		private final String uuid;
+		private final long timeout;
+
+		private DiscoveryThread(LatchedReference<StreamConnection> socketLatch,
+				String uuid, long timeout) {
+			this.socketLatch = socketLatch;
+			this.uuid = uuid;
+			this.timeout = timeout;
+		}
+
+		@Override
+		public void run() {
+			DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
+			long now = clock.currentTimeMillis();
+			long end = now + timeout;
+			while(now < end && running && !socketLatch.isSet()) {
+				if(!discoverySemaphore.tryAcquire()) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Another device discovery is in progress");
+					return;
+				}
+				try {
+					InvitationListener listener =
+							new InvitationListener(discoveryAgent, uuid);
+					discoveryAgent.startInquiry(GIAC, listener);
+					String url = listener.waitForUrl();
+					if(url != null) {
+						StreamConnection s = connect(url);
+						if(s == null) return;
+						if(LOG.isLoggable(INFO))
+							LOG.info("Outgoing connection");
+						if(!socketLatch.set(s)) {
+							if(LOG.isLoggable(INFO))
+								LOG.info("Closing redundant connection");
+							tryToClose(s);
+						}
+						return;
+					}
+				} catch(BluetoothStateException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					return;
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for URL");
+					Thread.currentThread().interrupt();
+					return;
+				} finally {
+					discoverySemaphore.release();
+				}
+			}
+		}
+
+		private void tryToClose(StreamConnection s) {
+			try {
+				s.close();
+			} catch(IOException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			}
+		}
+	}
+
+	private static class BluetoothListenerThread extends Thread {
+
+		private final LatchedReference<StreamConnection> socketLatch;
+		private final StreamConnectionNotifier serverSocket;
+
+		private BluetoothListenerThread(
+				LatchedReference<StreamConnection> socketLatch,
+				StreamConnectionNotifier serverSocket) {
+			this.socketLatch = socketLatch;
+			this.serverSocket = serverSocket;
+		}
+
+		@Override
+		public void run() {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Listening for invitation connections");
+			// Listen until a connection is received or the socket is closed
+			try {
+				StreamConnection s = serverSocket.acceptAndOpen();
+				if(LOG.isLoggable(INFO)) LOG.info("Incoming connection");
+				if(!socketLatch.set(s)) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Closing redundant connection");
+					s.close();
+				}
+			} catch(IOException e) {
+				// This is expected when the socket is closed
+				if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
+			}
+		}
+	}
 }