diff --git a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
index 204df03fe08ab8609d8c279fcaeeeed0f3d7596f..1cbd13eb256c3571caa019a51bd08e7221207364 100644
--- a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
+++ b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
@@ -57,7 +57,7 @@ implements InvitationListener {
 	private BluetoothWifiStateReceiver receiver = null;
 	private int localInvitationCode = -1, remoteInvitationCode = -1;
 	private int localConfirmationCode = -1, remoteConfirmationCode = -1;
-	private boolean connectionFailed = false;
+	private boolean connected = false, connectionFailed = false;
 	private boolean localCompared = false, remoteCompared = false;
 	private boolean localMatched = false, remoteMatched = false;
 	private String contactName = null;
@@ -109,6 +109,7 @@ implements InvitationListener {
 				remoteInvitationCode = s.getRemoteInvitationCode();
 				localConfirmationCode = s.getLocalConfirmationCode();
 				remoteConfirmationCode = s.getRemoteConfirmationCode();
+				connected = s.getConnected();
 				connectionFailed = s.getConnectionFailed();
 				localCompared = s.getLocalCompared();
 				remoteCompared = s.getRemoteCompared();
@@ -120,10 +121,12 @@ implements InvitationListener {
 					setView(new NetworkSetupView(this));
 				} else if(remoteInvitationCode == -1) {
 					setView(new InvitationCodeView(this));
-				} else if(localConfirmationCode == -1) {
-					setView(new ConnectionView(this));
 				} else if(connectionFailed) {
 					setView(new ConnectionFailedView(this));
+				} else if(connected && localConfirmationCode == -1) {
+					setView(new ConnectedView(this));
+				} else if(localConfirmationCode == -1) {
+					setView(new ConnectionView(this));
 				} else if(!localCompared) {
 					setView(new ConfirmationCodeView(this));
 				} else if(!remoteCompared) {
@@ -296,7 +299,26 @@ implements InvitationListener {
 		return contactName;
 	}
 
-	public void connectionSucceeded(final int localCode, final int remoteCode) {
+	public void connectionSucceeded() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				connected = true;
+				setView(new ConnectedView(AddContactActivity.this));
+			}
+		});
+	}
+
+	public void connectionFailed() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				connectionFailed = true;
+				setView(new ConnectionFailedView(AddContactActivity.this));
+			}
+		});
+	}
+
+	public void keyAgreementSucceeded(final int localCode,
+			final int remoteCode) {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				localConfirmationCode = localCode;
@@ -306,7 +328,7 @@ implements InvitationListener {
 		});
 	}
 
-	public void connectionFailed() {
+	public void keyAgreementFailed() {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				connectionFailed = true;
@@ -390,14 +412,22 @@ implements InvitationListener {
 			this.handle = handle;
 		}
 
-		public void connectionSucceeded(int localCode, int remoteCode) {
-			// Wait for remote confirmation to succeed or fail
+		public void connectionSucceeded() {
+			// Wait for key agreement to succeed or fail
 		}
 
 		public void connectionFailed() {
 			referenceManager.removeReference(handle, InvitationTask.class);
 		}
 
+		public void keyAgreementSucceeded(int localCode, int remoteCode) {
+			// Wait for remote confirmation to succeed or fail
+		}
+
+		public void keyAgreementFailed() {
+			referenceManager.removeReference(handle, InvitationTask.class);
+		}
+
 		public void remoteConfirmationSucceeded() {
 			// Wait for the pseudonym exchange to succeed or fail
 		}
diff --git a/briar-android/src/net/sf/briar/android/invitation/ConnectedView.java b/briar-android/src/net/sf/briar/android/invitation/ConnectedView.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba73343fb10f61a5ae044f072ce58e5e5909695b
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/invitation/ConnectedView.java
@@ -0,0 +1,34 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import net.sf.briar.R;
+import android.content.Context;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ConnectedView extends AddContactView {
+
+	ConnectedView(Context ctx) {
+		super(ctx);
+	}
+
+	void populate() {
+		removeAllViews();
+		Context ctx = getContext();
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		innerLayout.setOrientation(HORIZONTAL);
+		innerLayout.setGravity(CENTER);
+
+		ImageView icon = new ImageView(ctx);
+		icon.setImageResource(R.drawable.navigation_accept);
+		innerLayout.addView(icon);
+
+		TextView connected = new TextView(ctx);
+		connected.setTextSize(22);
+		connected.setPadding(10, 10, 10, 10);
+		connected.setText(R.string.connected_to_contact);
+		innerLayout.addView(connected);
+		addView(innerLayout);
+	}
+}
diff --git a/briar-api/src/net/sf/briar/api/invitation/InvitationListener.java b/briar-api/src/net/sf/briar/api/invitation/InvitationListener.java
index b7c3ccd2fc62e2ca7ef306e16f19b1a830c103bf..29726eacfd5737bc758e15f329f4923c53db0420 100644
--- a/briar-api/src/net/sf/briar/api/invitation/InvitationListener.java
+++ b/briar-api/src/net/sf/briar/api/invitation/InvitationListener.java
@@ -6,38 +6,42 @@ package net.sf.briar.api.invitation;
  */
 public interface InvitationListener {
 
-	/** Called if a connection is established and key agreement succeeds. */
-	void connectionSucceeded(int localCode, int remoteCode);
+	/** Called if a connection to the remote peer is established. */
+	void connectionSucceeded();
 
 	/**
-	 * Called if a connection cannot be established. This indicates that the
-	 * protocol has ended unsuccessfully.
+	 * Called if a connection to the remote peer cannot be established. This
+	 * indicates that the protocol has ended unsuccessfully.
 	 */
 	void connectionFailed();
 
+	/** Called if key agreement with the remote peer succeeds. */
+	void keyAgreementSucceeded(int localCode, int remoteCode);
+
 	/**
-	 * Informs the local peer that the remote peer's confirmation check
-	 * succeeded.
+	 * Called if key agreement with the remote peer fails or the connection is
+	 * lost. This indicates that the protocol has ended unsuccessfully.
 	 */
+	void keyAgreementFailed();
+
+	/** Called if the remote peer's confirmation check succeeds. */
 	void remoteConfirmationSucceeded();
 
 	/**
-	 * Informs the local peer that the remote peer's confirmation check did
-	 * not succeed, or the connection was lost during confirmation. This
-	 * indicates that the protocol has ended unsuccessfully.
+	 * Called if remote peer's confirmation check fails or the connection is
+	 * lost. This indicates that the protocol has ended unsuccessfully.
 	 */
 	void remoteConfirmationFailed();
 
 	/**
-	 * Informs the local peer of the name used by the remote peer. Called if
-	 * the exchange of pseudonyms succeeds. This indicates that the protocol
-	 * has ended successfully.
+	 * Called if the exchange of pseudonyms succeeds. This indicates that the
+	 * protocol has ended successfully.
 	 */
 	void pseudonymExchangeSucceeded(String remoteName);
 
 	/**
-	 * Called if the exchange of pseudonyms fails. This indicates that the
-	 * protocol has ended unsuccessfully.
+	 * Called if the exchange of pseudonyms fails or the connection is lost.
+	 * This indicates that the protocol has ended unsuccessfully.
 	 */
 	void pseudonymExchangeFailed();
 }
diff --git a/briar-api/src/net/sf/briar/api/invitation/InvitationState.java b/briar-api/src/net/sf/briar/api/invitation/InvitationState.java
index 0a65863d38d680a551a2173b539cd2aaac9790df..0e03efe80235b8215e2dcac8cace743d2b9ae2f0 100644
--- a/briar-api/src/net/sf/briar/api/invitation/InvitationState.java
+++ b/briar-api/src/net/sf/briar/api/invitation/InvitationState.java
@@ -4,20 +4,21 @@ public class InvitationState {
 
 	private final int localInvitationCode, remoteInvitationCode;
 	private final int localConfirmationCode, remoteConfirmationCode;
-	private final boolean connectionFailed;
+	private final boolean connected, connectionFailed;
 	private final boolean localCompared, remoteCompared;
 	private final boolean localMatched, remoteMatched;
 	private final String contactName;
 
 	public InvitationState(int localInvitationCode, int remoteInvitationCode,
 			int localConfirmationCode, int remoteConfirmationCode,
-			boolean connectionFailed, boolean localCompared,
+			boolean connected, boolean connectionFailed, boolean localCompared,
 			boolean remoteCompared, boolean localMatched,
 			boolean remoteMatched, String contactName) {
 		this.localInvitationCode = localInvitationCode;
 		this.remoteInvitationCode = remoteInvitationCode;
 		this.localConfirmationCode = localConfirmationCode;
 		this.remoteConfirmationCode = remoteConfirmationCode;
+		this.connected = connected;
 		this.connectionFailed = connectionFailed;
 		this.localCompared = localCompared;
 		this.remoteCompared = remoteCompared;
@@ -42,6 +43,10 @@ public class InvitationState {
 		return remoteConfirmationCode;
 	}
 
+	public boolean getConnected() {
+		return connected;
+	}
+
 	public boolean getConnectionFailed() {
 		return connectionFailed;
 	}
diff --git a/briar-core/src/net/sf/briar/invitation/AliceConnector.java b/briar-core/src/net/sf/briar/invitation/AliceConnector.java
index ad49087a1d0c1a0374ee2ba3da920b80dce0b090..580195cd1c070109050c6604d6d62fb6368f3a66 100644
--- a/briar-core/src/net/sf/briar/invitation/AliceConnector.java
+++ b/briar-core/src/net/sf/briar/invitation/AliceConnector.java
@@ -91,10 +91,12 @@ class AliceConnector extends Connector {
 			secret = deriveMasterSecret(hash, key, true);
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			group.keyAgreementFailed();
 			tryToClose(conn, true);
 			return;
 		} catch(GeneralSecurityException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			group.keyAgreementFailed();
 			tryToClose(conn, true);
 			return;
 		}
@@ -102,7 +104,7 @@ class AliceConnector extends Connector {
 		if(LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
 		int[] codes = crypto.deriveConfirmationCodes(secret);
 		int aliceCode = codes[0], bobCode = codes[1];
-		group.connectionSucceeded(aliceCode, bobCode);
+		group.keyAgreementSucceeded(aliceCode, bobCode);
 		// Exchange confirmation results
 		try {
 			sendConfirmation(w);
diff --git a/briar-core/src/net/sf/briar/invitation/BobConnector.java b/briar-core/src/net/sf/briar/invitation/BobConnector.java
index a86b1ea01359756f3a289f795a3f620cd9f3dd46..8d7d2ad09f58f23e4a69aec280d8dc622a27082b 100644
--- a/briar-core/src/net/sf/briar/invitation/BobConnector.java
+++ b/briar-core/src/net/sf/briar/invitation/BobConnector.java
@@ -91,10 +91,12 @@ class BobConnector extends Connector {
 			secret = deriveMasterSecret(hash, key, false);
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			group.keyAgreementFailed();
 			tryToClose(conn, true);
 			return;
 		} catch(GeneralSecurityException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			group.keyAgreementFailed();
 			tryToClose(conn, true);
 			return;
 		}
@@ -102,7 +104,7 @@ class BobConnector extends Connector {
 		if(LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
 		int[] codes = crypto.deriveConfirmationCodes(secret);
 		int aliceCode = codes[0], bobCode = codes[1];
-		group.connectionSucceeded(bobCode, aliceCode);
+		group.keyAgreementSucceeded(bobCode, aliceCode);
 		// Exchange confirmation results
 		try {
 			if(receiveConfirmation(r)) group.remoteConfirmationSucceeded();
diff --git a/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java b/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java
index a96b1b556e94adab2fd6c16cb44a25f8f5b5dc6d..47f0beb6053c9dd1f7b9b3509cae760ca40449f9 100644
--- a/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java
+++ b/briar-core/src/net/sf/briar/invitation/ConnectorGroup.java
@@ -101,7 +101,7 @@ class ConnectorGroup extends Thread implements InvitationTask {
 	public synchronized InvitationState addListener(InvitationListener l) {
 		listeners.add(l);
 		return new InvitationState(localInvitationCode, remoteInvitationCode,
-				localConfirmationCode, remoteConfirmationCode,
+				localConfirmationCode, remoteConfirmationCode, connected.get(),
 				connectionFailed, localCompared, remoteCompared, localMatched,
 				remoteMatched, remoteName);
 	}
@@ -202,16 +202,24 @@ class ConnectorGroup extends Thread implements InvitationTask {
 	}
 
 	boolean getAndSetConnected() {
-		return connected.getAndSet(true);
+		boolean redundant = connected.getAndSet(true);
+		if(!redundant) {
+			for(InvitationListener l : listeners) l.connectionSucceeded();
+		}
+		return redundant;
 	}
 
-	void connectionSucceeded(int localCode, int remoteCode) {
+	void keyAgreementSucceeded(int localCode, int remoteCode) {
 		synchronized(this) {
 			localConfirmationCode = localCode;
 			remoteConfirmationCode = remoteCode;
 		}
 		for(InvitationListener l : listeners)
-			l.connectionSucceeded(localCode, remoteCode);
+			l.keyAgreementSucceeded(localCode, remoteCode);
+	}
+
+	void keyAgreementFailed() {
+		for(InvitationListener l : listeners) l.keyAgreementFailed();
 	}
 
 	void remoteConfirmationSucceeded() {