diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f58b344443d5309ba57c7ca4d620b326e7fc3682..a81aeebad8febddce1759c1e8dabbf8405e7ec9b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -32,35 +32,7 @@
 			</intent-filter>
 		</activity>
 		<activity
-			android:name=".android.invitation.NetworkSetupActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.InvitationCodeActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.ConnectionActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.ConnectionFailedActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.ConfirmationCodeActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.WaitForContactActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.CodesDoNotMatchActivity"
-			android:label="@string/add_a_contact" >
-		</activity>
-		<activity
-			android:name=".android.invitation.ContactAddedActivity"
+			android:name=".android.invitation.AddContactActivity"
 			android:label="@string/add_a_contact" >
 		</activity>
 	</application>
diff --git a/res/values/roboguice.xml b/res/values/roboguice.xml
index 82fd88cbc514fa058a0616e8a23984153a80e614..a20130d0f18d517339b19bd52086d43e428010cc 100644
--- a/res/values/roboguice.xml
+++ b/res/values/roboguice.xml
@@ -3,10 +3,10 @@
 	<string-array name="roboguice_modules">
 		<item>net.sf.briar.android.AndroidModule</item>
 		<item>net.sf.briar.android.helloworld.HelloWorldModule</item>
-		<item>net.sf.briar.android.invitation.AndroidInvitationModule</item>
 		<item>net.sf.briar.clock.ClockModule</item>
 		<item>net.sf.briar.crypto.CryptoModule</item>
 		<item>net.sf.briar.db.DatabaseModule</item>
+		<item>net.sf.briar.invitation.InvitationModule</item>
 		<item>net.sf.briar.lifecycle.LifecycleModule</item>
 		<item>net.sf.briar.plugins.PluginsModule</item>
 		<item>net.sf.briar.protocol.ProtocolModule</item>
diff --git a/src/net/sf/briar/android/helloworld/HelloWorldActivity.java b/src/net/sf/briar/android/helloworld/HelloWorldActivity.java
index 794495d24dd85262ffd52dde1e1eb6ea7c2773a9..98f9e52d5cb3c330dcae30e33d20f43883acc9ab 100644
--- a/src/net/sf/briar/android/helloworld/HelloWorldActivity.java
+++ b/src/net/sf/briar/android/helloworld/HelloWorldActivity.java
@@ -9,7 +9,7 @@ import static java.util.logging.Level.INFO;
 import java.util.logging.Logger;
 
 import net.sf.briar.R;
-import net.sf.briar.android.invitation.NetworkSetupActivity;
+import net.sf.briar.android.invitation.AddContactActivity;
 import roboguice.activity.RoboActivity;
 import android.content.Intent;
 import android.os.Bundle;
@@ -66,6 +66,6 @@ implements OnClickListener {
 	}
 
 	public void onClick(View view) {
-		startActivity(new Intent(this, NetworkSetupActivity.class));
+		startActivity(new Intent(this, AddContactActivity.class));
 	}
 }
diff --git a/src/net/sf/briar/android/invitation/AddContactActivity.java b/src/net/sf/briar/android/invitation/AddContactActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..20c7cdd63f5fb05e9493cee83d73111604a77766
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/AddContactActivity.java
@@ -0,0 +1,128 @@
+package net.sf.briar.android.invitation;
+
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.invitation.ConfirmationCallback;
+import net.sf.briar.api.invitation.ConnectionCallback;
+import net.sf.briar.api.invitation.InvitationManager;
+import roboguice.activity.RoboActivity;
+
+import com.google.inject.Inject;
+
+public class AddContactActivity extends RoboActivity
+implements ConnectionCallback, ConfirmationCallback {
+
+	@Inject private CryptoComponent crypto;
+	@Inject private InvitationManager invitationManager;
+
+	// All of the following must be accessed on the UI thread
+	private AddContactView view = null;
+	private String networkName = null;
+	private boolean useBluetooth = false;
+	private int localInvitationCode = -1;
+	private int localConfirmationCode = -1,  remoteConfirmationCode = -1;
+	private ConfirmationCallback callback = null;
+	private boolean localMatched = false;
+	private boolean remoteCompared = false, remoteMatched = false;
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		if(view == null) setView(new NetworkSetupView(this));
+		else view.populate();
+	}
+
+	void setView(AddContactView view) {
+		this.view = view;
+		view.init(this);
+		setContentView(view);
+	}
+
+	void setNetworkName(String networkName) {
+		this.networkName = networkName;
+	}
+
+	String getNetworkName() {
+		return networkName;
+	}
+
+	void setUseBluetooth(boolean useBluetooth) {
+		this.useBluetooth = useBluetooth;
+	}
+
+	boolean getUseBluetooth() {
+		return useBluetooth;
+	}
+
+	int generateLocalInvitationCode() {
+		localInvitationCode = crypto.generateInvitationCode();
+		return localInvitationCode;
+	}
+
+	int getLocalInvitationCode() {
+		return localInvitationCode;
+	}
+
+	void remoteInvitationCodeEntered(int code) {
+		setView(new ConnectionView(this));
+		localMatched = remoteCompared = remoteMatched = false;
+		invitationManager.connect(localInvitationCode, code, this);
+	}
+
+	int getLocalConfirmationCode() {
+		return localConfirmationCode;
+	}
+
+	void remoteConfirmationCodeEntered(int code) {
+		if(code == remoteConfirmationCode) {
+			localMatched = true;
+			if(remoteMatched) setView(new ContactAddedView(this));
+			else if(remoteCompared) setView(new CodesDoNotMatchView(this));
+			else setView(new WaitForContactView(this));
+			callback.codesMatch();
+		} else {
+			setView(new CodesDoNotMatchView(this));
+			callback.codesDoNotMatch();
+		}
+	}
+
+	public void connectionEstablished(final int localCode, final int remoteCode,
+			final ConfirmationCallback c) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				localConfirmationCode = localCode;
+				remoteConfirmationCode = remoteCode;
+				callback = c;
+				setView(new ConfirmationCodeView(AddContactActivity.this));
+			}
+		});
+	}
+
+	public void connectionNotEstablished() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				setView(new ConnectionFailedView(AddContactActivity.this));
+			}
+		});
+	}
+
+	public void codesMatch() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				remoteCompared = true;
+				remoteMatched = true;
+				if(localMatched)
+					setView(new ContactAddedView(AddContactActivity.this));
+			}
+		});
+	}
+
+	public void codesDoNotMatch() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				remoteCompared = true;
+				if(localMatched)
+					setView(new CodesDoNotMatchView(AddContactActivity.this));
+			}
+		});
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/AddContactView.java b/src/net/sf/briar/android/invitation/AddContactView.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd90bc0fd8edbcda11cc3dbd48d2a3fb4a122ae9
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/AddContactView.java
@@ -0,0 +1,25 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import android.content.Context;
+import android.widget.LinearLayout;
+
+abstract class AddContactView extends LinearLayout {
+
+	protected AddContactActivity container = null;
+
+	AddContactView(Context context) {
+		super(context);
+	}
+
+	void init(AddContactActivity container) {
+		this.container = container;
+		setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+		setOrientation(VERTICAL);
+		setGravity(CENTER_HORIZONTAL);
+		populate();
+	}
+
+	abstract void populate();
+}
diff --git a/src/net/sf/briar/android/invitation/AndroidInvitationModule.java b/src/net/sf/briar/android/invitation/AndroidInvitationModule.java
deleted file mode 100644
index c917e2256b53be9418b3a600dd3d5ffc63e835b0..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/AndroidInvitationModule.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import javax.inject.Singleton;
-
-import com.google.inject.AbstractModule;
-
-public class AndroidInvitationModule extends AbstractModule {
-
-	@Override
-	protected void configure() {
-		bind(InvitationManager.class).to(InvitationManagerImpl.class).in(
-				Singleton.class);
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/CodeEntryListener.java b/src/net/sf/briar/android/invitation/CodeEntryListener.java
index 1a8f7b3d8d58a9925c030bdf83d428ed57023c6c..43c324564409764aa75f54dec4ea15a55dcaa022 100644
--- a/src/net/sf/briar/android/invitation/CodeEntryListener.java
+++ b/src/net/sf/briar/android/invitation/CodeEntryListener.java
@@ -2,5 +2,5 @@ package net.sf.briar.android.invitation;
 
 interface CodeEntryListener {
 
-	void codeEntered(String code);
+	void codeEntered(int remoteCode);
 }
diff --git a/src/net/sf/briar/android/invitation/CodeEntryWidget.java b/src/net/sf/briar/android/invitation/CodeEntryWidget.java
index 788e23b85a16b0ca594ff969217347d2fb504acc..5f129481b5d47f9b42249cf958de13632754f2bd 100644
--- a/src/net/sf/briar/android/invitation/CodeEntryWidget.java
+++ b/src/net/sf/briar/android/invitation/CodeEntryWidget.java
@@ -3,6 +3,7 @@ package net.sf.briar.android.invitation;
 import static android.text.InputType.TYPE_CLASS_NUMBER;
 import static android.view.Gravity.CENTER;
 import static android.view.Gravity.CENTER_HORIZONTAL;
+import static net.sf.briar.api.plugins.InvitationConstants.MAX_CODE;
 import net.sf.briar.R;
 import android.content.Context;
 import android.view.KeyEvent;
@@ -53,6 +54,7 @@ OnEditorActionListener, OnClickListener {
 		codeEntry.setMaxEms(5);
 		codeEntry.setMaxLines(1);
 		codeEntry.setInputType(TYPE_CLASS_NUMBER);
+		codeEntry.requestFocus();
 
 		LinearLayout innerLayout = new LinearLayout(ctx);
 		innerLayout.setOrientation(HORIZONTAL);
@@ -63,16 +65,24 @@ OnEditorActionListener, OnClickListener {
 	}
 
 	public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
-		validateAndReturnCode();
+		if(!validateAndReturnCode()) codeEntry.setText("");
 		return true;
 	}
 
 	public void onClick(View view) {
-		validateAndReturnCode();
+		if(!validateAndReturnCode()) codeEntry.setText("");
 	}
 
-	private void validateAndReturnCode() {
-		CharSequence code = codeEntry.getText();
-		if(code.length() == 6) listener.codeEntered(code.toString());
+	private boolean validateAndReturnCode() {
+		String remoteCodeString = codeEntry.getText().toString();
+		int remoteCode;
+		try {
+			remoteCode = Integer.valueOf(remoteCodeString);
+		} catch(NumberFormatException e) {
+			return false;
+		}
+		if(remoteCode < 0 || remoteCode > MAX_CODE) return false;
+		listener.codeEntered(remoteCode);
+		return true;
 	}
 }
diff --git a/src/net/sf/briar/android/invitation/CodesDoNotMatchActivity.java b/src/net/sf/briar/android/invitation/CodesDoNotMatchActivity.java
deleted file mode 100644
index 861a80c7008c2d192105907ef27cb9cd279b114e..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/CodesDoNotMatchActivity.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-public class CodesDoNotMatchActivity extends Activity
-implements OnClickListener {
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		LinearLayout innerLayout = new LinearLayout(this);
-		innerLayout.setOrientation(HORIZONTAL);
-		innerLayout.setGravity(CENTER);
-
-		ImageView icon = new ImageView(this);
-		icon.setPadding(10, 10, 10, 10);
-		icon.setImageResource(R.drawable.alerts_and_states_error);
-		innerLayout.addView(icon);
-
-		TextView failed = new TextView(this);
-		failed.setTextSize(20);
-		failed.setText(R.string.codes_do_not_match);
-		innerLayout.addView(failed);
-		layout.addView(innerLayout);
-
-		TextView interfering = new TextView(this);
-		interfering.setGravity(CENTER_HORIZONTAL);
-		interfering.setPadding(0, 0, 0, 10);
-		interfering.setText(R.string.interfering);
-		layout.addView(interfering);
-
-		Button tryAgain = new Button(this);
-		LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
-		tryAgain.setLayoutParams(lp);
-		tryAgain.setText(R.string.try_again_button);
-		tryAgain.setOnClickListener(this);
-		layout.addView(tryAgain);
-
-		setContentView(layout);
-	}
-
-	public void onClick(View view) {
-		Intent intent = new Intent(this, InvitationCodeActivity.class);
-		intent.putExtras(getIntent().getExtras());
-		startActivity(intent);
-		finish();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/CodesDoNotMatchView.java b/src/net/sf/briar/android/invitation/CodesDoNotMatchView.java
new file mode 100644
index 0000000000000000000000000000000000000000..d801d01ae91cc56df37b5c160f62ba6a5d33264b
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/CodesDoNotMatchView.java
@@ -0,0 +1,58 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import net.sf.briar.R;
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class CodesDoNotMatchView extends AddContactView
+implements OnClickListener {
+
+	CodesDoNotMatchView(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.setPadding(10, 10, 10, 10);
+		icon.setImageResource(R.drawable.alerts_and_states_error);
+		innerLayout.addView(icon);
+
+		TextView failed = new TextView(ctx);
+		failed.setTextSize(20);
+		failed.setText(R.string.codes_do_not_match);
+		innerLayout.addView(failed);
+		addView(innerLayout);
+
+		TextView interfering = new TextView(ctx);
+		interfering.setGravity(CENTER_HORIZONTAL);
+		interfering.setPadding(0, 0, 0, 10);
+		interfering.setText(R.string.interfering);
+		addView(interfering);
+
+		Button tryAgain = new Button(ctx);
+		LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+		tryAgain.setLayoutParams(lp);
+		tryAgain.setText(R.string.try_again_button);
+		tryAgain.setOnClickListener(this);
+		addView(tryAgain);
+	}
+
+	public void onClick(View view) {
+		// Try again
+		container.setView(new NetworkSetupView(container));
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/ConfirmationCodeActivity.java b/src/net/sf/briar/android/invitation/ConfirmationCodeActivity.java
deleted file mode 100644
index 220548b58019b071d9b503b7cd39ccad58c05603..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/ConfirmationCodeActivity.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import roboguice.activity.RoboActivity;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.google.inject.Inject;
-
-public class ConfirmationCodeActivity extends RoboActivity
-implements CodeEntryListener {
-
-	@Inject private InvitationManager manager;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		LinearLayout innerLayout = new LinearLayout(this);
-		innerLayout.setOrientation(HORIZONTAL);
-		innerLayout.setGravity(CENTER);
-
-		ImageView icon = new ImageView(this);
-		icon.setPadding(10, 10, 10, 10);
-		icon.setImageResource(R.drawable.navigation_accept);
-		innerLayout.addView(icon);
-
-		TextView connected = new TextView(this);
-		connected.setTextSize(20);
-		connected.setText(R.string.connected_to_contact);
-		innerLayout.addView(connected);
-		layout.addView(innerLayout);
-
-		TextView yourCode = new TextView(this);
-		yourCode.setGravity(CENTER_HORIZONTAL);
-		yourCode.setText(R.string.your_confirmation_code);
-		layout.addView(yourCode);
-
-		TextView code = new TextView(this);
-		code.setGravity(CENTER_HORIZONTAL);
-		code.setTextSize(50);
-		code.setText(manager.getLocalConfirmationCode());
-		layout.addView(code);
-
-		CodeEntryWidget codeEntry = new CodeEntryWidget(this);
-		Resources res = getResources();
-		codeEntry.init(this, res.getString(R.string.enter_confirmation_code));
-		layout.addView(codeEntry);
-
-		setContentView(layout);
-	}
-
-	public void codeEntered(String code) {
-		if(code.equals(manager.getRemoteConfirmationCode())) {
-			Intent intent = new Intent(this, WaitForContactActivity.class);
-			intent.putExtras(getIntent().getExtras());
-			startActivity(intent);
-		} else {
-			Intent intent = new Intent(this, CodesDoNotMatchActivity.class);
-			intent.putExtras(getIntent().getExtras());
-			startActivity(intent);
-		}
-		finish();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/ConfirmationCodeView.java b/src/net/sf/briar/android/invitation/ConfirmationCodeView.java
new file mode 100644
index 0000000000000000000000000000000000000000..04688795f91a87905c027d1ec30b487363ea236f
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConfirmationCodeView.java
@@ -0,0 +1,58 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.content.Context;
+import android.content.res.Resources;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ConfirmationCodeView extends AddContactView
+implements CodeEntryListener {
+
+	ConfirmationCodeView(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.setPadding(10, 10, 10, 10);
+		icon.setImageResource(R.drawable.navigation_accept);
+		innerLayout.addView(icon);
+
+		TextView connected = new TextView(ctx);
+		connected.setTextSize(20);
+		connected.setText(R.string.connected_to_contact);
+		innerLayout.addView(connected);
+		addView(innerLayout);
+
+		TextView yourCode = new TextView(ctx);
+		yourCode.setGravity(CENTER_HORIZONTAL);
+		yourCode.setText(R.string.your_confirmation_code);
+		addView(yourCode);
+
+		TextView code = new TextView(ctx);
+		code.setGravity(CENTER_HORIZONTAL);
+		code.setTextSize(50);
+		int localCode = container.getLocalConfirmationCode();
+		code.setText(String.format("%06d", localCode));
+		addView(code);
+
+		CodeEntryWidget codeEntry = new CodeEntryWidget(ctx);
+		Resources res = getResources();
+		codeEntry.init(this, res.getString(R.string.enter_confirmation_code));
+		addView(codeEntry);
+	}
+
+	public void codeEntered(int remoteCode) {
+		container.remoteConfirmationCodeEntered(remoteCode);
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/ConfirmationListener.java b/src/net/sf/briar/android/invitation/ConfirmationListener.java
deleted file mode 100644
index af2024676a4731cb7aa4f73479fa7fa50cbda756..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/ConfirmationListener.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.sf.briar.android.invitation;
-
-interface ConfirmationListener {
-
-	void confirmationReceived();
-
-	void confirmationNotReceived();
-}
diff --git a/src/net/sf/briar/android/invitation/ConnectionActivity.java b/src/net/sf/briar/android/invitation/ConnectionActivity.java
deleted file mode 100644
index 5ed8b80d38141492394d2d0d1bb63ab60c205f87..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/ConnectionActivity.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import roboguice.activity.RoboActivity;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import com.google.inject.Inject;
-
-public class ConnectionActivity extends RoboActivity
-implements ConnectionListener {
-
-	@Inject private InvitationManager manager;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		Bundle b = getIntent().getExtras();
-		String networkName = b.getString(
-				"net.sf.briar.android.invitation.NETWORK_NAME");
-		boolean useBluetooth = b.getBoolean(
-				"net.sf.briar.android.invitation.USE_BLUETOOTH");
-
-		TextView yourCode = new TextView(this);
-		yourCode.setGravity(CENTER_HORIZONTAL);
-		yourCode.setText(R.string.your_invitation_code);
-		layout.addView(yourCode);
-
-		TextView code = new TextView(this);
-		code.setGravity(CENTER_HORIZONTAL);
-		code.setTextSize(50);
-		code.setText(manager.getLocalInvitationCode());
-		layout.addView(code);
-
-		if(networkName != null) {
-			LinearLayout innerLayout = new LinearLayout(this);
-			innerLayout.setOrientation(HORIZONTAL);
-			innerLayout.setGravity(CENTER);
-
-			ProgressBar progress = new ProgressBar(this);
-			progress.setIndeterminate(true);
-			progress.setPadding(0, 10, 10, 0);
-			innerLayout.addView(progress);
-
-			TextView connecting = new TextView(this);
-			Resources res = getResources();
-			String connectingVia = res.getString(R.string.connecting_wifi);
-			connecting.setText(String.format(connectingVia, networkName));
-			innerLayout.addView(connecting);
-
-			layout.addView(innerLayout);
-			manager.startWifiConnectionWorker(this);
-		}
-
-		if(useBluetooth) {
-			LinearLayout innerLayout = new LinearLayout(this);
-			innerLayout.setOrientation(HORIZONTAL);
-			innerLayout.setGravity(CENTER);
-
-			ProgressBar progress = new ProgressBar(this);
-			progress.setPadding(0, 10, 10, 0);
-			progress.setIndeterminate(true);
-			innerLayout.addView(progress);
-
-			TextView connecting = new TextView(this);
-			connecting.setText(R.string.connecting_bluetooth);
-			innerLayout.addView(connecting);
-
-			layout.addView(innerLayout);
-			manager.startBluetoothConnectionWorker(this);
-		}
-
-		setContentView(layout);
-
-		manager.tryToConnect(this);
-	}
-
-	public void connectionEstablished() {
-		final Intent intent = new Intent(this, ConfirmationCodeActivity.class);
-		intent.putExtras(getIntent().getExtras());
-		runOnUiThread(new Runnable() {
-			public void run() {
-				startActivity(intent);
-				finish();
-			}
-		});
-	}
-
-	public void connectionNotEstablished() {
-		final Intent intent = new Intent(this, ConnectionFailedActivity.class);
-		runOnUiThread(new Runnable() {
-			public void run() {
-				startActivity(intent);
-				finish();
-			}
-		});
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/ConnectionFailedActivity.java b/src/net/sf/briar/android/invitation/ConnectionFailedActivity.java
deleted file mode 100644
index a18e90a1995e4b807d7c2b7621e002b476d1cda7..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/ConnectionFailedActivity.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-public class ConnectionFailedActivity extends Activity
-implements WifiStateListener, BluetoothStateListener, OnClickListener {
-
-	private WifiWidget wifi = null;
-	private BluetoothWidget bluetooth = null;
-	private Button tryAgainButton = null;
-	private String networkName = null;
-	private boolean useBluetooth = false;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		LinearLayout innerLayout = new LinearLayout(this);
-		innerLayout.setOrientation(HORIZONTAL);
-		innerLayout.setGravity(CENTER);
-
-		ImageView icon = new ImageView(this);
-		icon.setPadding(10, 10, 10, 10);
-		icon.setImageResource(R.drawable.alerts_and_states_error);
-		innerLayout.addView(icon);
-
-		TextView failed = new TextView(this);
-		failed.setTextSize(20);
-		failed.setText(R.string.connection_failed);
-		innerLayout.addView(failed);
-		layout.addView(innerLayout);
-
-		TextView checkNetwork = new TextView(this);
-		checkNetwork.setGravity(CENTER_HORIZONTAL);
-		checkNetwork.setText(R.string.check_same_network);
-		layout.addView(checkNetwork);
-
-		wifi = new WifiWidget(this);
-		wifi.init(this);
-		layout.addView(wifi);
-
-		bluetooth = new BluetoothWidget(this);
-		bluetooth.init(this);
-		layout.addView(bluetooth);
-
-		tryAgainButton = new Button(this);
-		LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
-		tryAgainButton.setLayoutParams(lp);
-		tryAgainButton.setText(R.string.try_again_button);
-		tryAgainButton.setOnClickListener(this);
-		enabledOrDisableTryAgainButton();
-		layout.addView(tryAgainButton);
-
-		setContentView(layout);
-	}
-
-	@Override
-	public void onResume() {
-		super.onResume();
-		wifi.populate();
-		bluetooth.populate();
-	}
-
-	public void wifiStateChanged(String networkName) {
-		this.networkName = networkName;
-		enabledOrDisableTryAgainButton();
-	}
-
-	public void bluetoothStateChanged(boolean enabled) {
-		useBluetooth = enabled;
-		enabledOrDisableTryAgainButton();
-	}
-
-	private void enabledOrDisableTryAgainButton() {
-		if(tryAgainButton == null) return; // Activity not created yet
-		if(useBluetooth || networkName != null) tryAgainButton.setEnabled(true);
-		else tryAgainButton.setEnabled(false);
-	}
-
-	public void onClick(View view) {
-		Intent intent = new Intent(this, InvitationCodeActivity.class);
-		intent.putExtra("net.sf.briar.android.invitation.NETWORK_NAME",
-				networkName);
-		intent.putExtra("net.sf.briar.android.invitation.USE_BLUETOOTH",
-				useBluetooth);
-		startActivity(intent);
-		finish();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/ConnectionFailedView.java b/src/net/sf/briar/android/invitation/ConnectionFailedView.java
new file mode 100644
index 0000000000000000000000000000000000000000..970314cd942b529413491f0289dfa2fbcdc8a9ac
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConnectionFailedView.java
@@ -0,0 +1,86 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import net.sf.briar.R;
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ConnectionFailedView extends AddContactView
+implements WifiStateListener, BluetoothStateListener, OnClickListener {
+
+	private Button tryAgainButton = null;
+
+	ConnectionFailedView(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.setPadding(10, 10, 10, 10);
+		icon.setImageResource(R.drawable.alerts_and_states_error);
+		innerLayout.addView(icon);
+
+		TextView failed = new TextView(ctx);
+		failed.setTextSize(20);
+		failed.setText(R.string.connection_failed);
+		innerLayout.addView(failed);
+		addView(innerLayout);
+
+		TextView checkNetwork = new TextView(ctx);
+		checkNetwork.setGravity(CENTER_HORIZONTAL);
+		checkNetwork.setText(R.string.check_same_network);
+		addView(checkNetwork);
+
+		WifiWidget wifi = new WifiWidget(ctx);
+		wifi.init(this);
+		addView(wifi);
+
+		BluetoothWidget bluetooth = new BluetoothWidget(ctx);
+		bluetooth.init(this);
+		addView(bluetooth);
+
+		tryAgainButton = new Button(ctx);
+		LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+		tryAgainButton.setLayoutParams(lp);
+		tryAgainButton.setText(R.string.try_again_button);
+		tryAgainButton.setOnClickListener(this);
+		enabledOrDisableTryAgainButton();
+		addView(tryAgainButton);
+	}
+
+	public void wifiStateChanged(String networkName) {
+		container.setNetworkName(networkName);
+		enabledOrDisableTryAgainButton();
+	}
+
+	public void bluetoothStateChanged(boolean enabled) {
+		container.setUseBluetooth(enabled);
+		enabledOrDisableTryAgainButton();
+	}
+
+	private void enabledOrDisableTryAgainButton() {
+		if(tryAgainButton == null) return; // Activity not created yet
+		boolean useBluetooth = container.getUseBluetooth();
+		String networkName = container.getNetworkName();
+		if(useBluetooth || networkName != null) tryAgainButton.setEnabled(true);
+		else tryAgainButton.setEnabled(false);
+	}
+
+	public void onClick(View view) {
+		// Try again
+		container.setView(new InvitationCodeView(container));
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/ConnectionListener.java b/src/net/sf/briar/android/invitation/ConnectionListener.java
deleted file mode 100644
index a1bc643dc3bdc53b17f748759c824104feece6a2..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/ConnectionListener.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.sf.briar.android.invitation;
-
-interface ConnectionListener {
-
-	void connectionEstablished();
-
-	void connectionNotEstablished();
-}
diff --git a/src/net/sf/briar/android/invitation/ConnectionView.java b/src/net/sf/briar/android/invitation/ConnectionView.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a35c26d61b5500da2f610e891e5c3e0d43496cf
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConnectionView.java
@@ -0,0 +1,71 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.content.Context;
+import android.content.res.Resources;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class ConnectionView extends AddContactView {
+
+	ConnectionView(Context ctx) {
+		super(ctx);
+	}
+
+	void populate() {
+		removeAllViews();
+		Context ctx = getContext();
+		TextView yourCode = new TextView(ctx);
+		yourCode.setGravity(CENTER_HORIZONTAL);
+		yourCode.setText(R.string.your_invitation_code);
+		addView(yourCode);
+
+		TextView code = new TextView(ctx);
+		code.setGravity(CENTER_HORIZONTAL);
+		code.setTextSize(50);
+		int localCode = container.getLocalInvitationCode();
+		code.setText(String.format("%06d", localCode));
+		addView(code);
+
+		String networkName = container.getNetworkName();
+		if(networkName != null) {
+			LinearLayout innerLayout = new LinearLayout(ctx);
+			innerLayout.setOrientation(HORIZONTAL);
+			innerLayout.setGravity(CENTER);
+
+			ProgressBar progress = new ProgressBar(ctx);
+			progress.setIndeterminate(true);
+			progress.setPadding(0, 10, 10, 0);
+			innerLayout.addView(progress);
+
+			TextView connecting = new TextView(ctx);
+			Resources res = getResources();
+			String connectingVia = res.getString(R.string.connecting_wifi);
+			connecting.setText(String.format(connectingVia, networkName));
+			innerLayout.addView(connecting);
+
+			addView(innerLayout);
+		}
+
+		boolean useBluetooth = container.getUseBluetooth();
+		if(useBluetooth) {
+			LinearLayout innerLayout = new LinearLayout(ctx);
+			innerLayout.setOrientation(HORIZONTAL);
+			innerLayout.setGravity(CENTER);
+
+			ProgressBar progress = new ProgressBar(ctx);
+			progress.setPadding(0, 10, 10, 0);
+			progress.setIndeterminate(true);
+			innerLayout.addView(progress);
+
+			TextView connecting = new TextView(ctx);
+			connecting.setText(R.string.connecting_bluetooth);
+			innerLayout.addView(connecting);
+
+			addView(innerLayout);
+		}
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/ContactAddedActivity.java b/src/net/sf/briar/android/invitation/ContactAddedView.java
similarity index 56%
rename from src/net/sf/briar/android/invitation/ContactAddedActivity.java
rename to src/net/sf/briar/android/invitation/ContactAddedView.java
index 8f75032806bafa7105dff4a45e48bace5888cfe1..25ad3a3ab85e3d6b53d0f3836774e7bbbf594c1c 100644
--- a/src/net/sf/briar/android/invitation/ContactAddedActivity.java
+++ b/src/net/sf/briar/android/invitation/ContactAddedView.java
@@ -2,17 +2,11 @@ package net.sf.briar.android.invitation;
 
 import static android.view.Gravity.CENTER;
 import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
 import net.sf.briar.R;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
+import android.content.Context;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.ViewGroup.LayoutParams;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ImageView;
@@ -20,43 +14,42 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 
-public class ContactAddedActivity extends Activity implements OnClickListener,
+public class ContactAddedView extends AddContactView implements OnClickListener,
 OnEditorActionListener {
 
-	private volatile Button done = null;
+	private Button done = null;
 
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
+	ContactAddedView(Context ctx) {
+		super(ctx);
+	}
 
-		LinearLayout innerLayout = new LinearLayout(this);
+	void populate() {
+		removeAllViews();
+		Context ctx = getContext();
+		LinearLayout innerLayout = new LinearLayout(ctx);
 		innerLayout.setOrientation(HORIZONTAL);
 		innerLayout.setGravity(CENTER);
 
-		ImageView icon = new ImageView(this);
+		ImageView icon = new ImageView(ctx);
 		icon.setImageResource(R.drawable.navigation_accept);
 		icon.setPadding(10, 10, 10, 10);
 		innerLayout.addView(icon);
 
-		TextView failed = new TextView(this);
+		TextView failed = new TextView(ctx);
 		failed.setText(R.string.contact_added);
 		failed.setTextSize(20);
 		innerLayout.addView(failed);
-		layout.addView(innerLayout);
+		addView(innerLayout);
 
-		TextView enterNickname = new TextView(this);
+		TextView enterNickname = new TextView(ctx);
 		enterNickname.setGravity(CENTER_HORIZONTAL);
 		enterNickname.setText(R.string.enter_nickname);
-		layout.addView(enterNickname);
+		addView(enterNickname);
 
-		final Button addAnother = new Button(this);
-		final Button done = new Button(this);
+		final Button addAnother = new Button(ctx);
+		final Button done = new Button(ctx);
 		this.done = done;
-		EditText nicknameEntry = new EditText(this) {
+		EditText nicknameEntry = new EditText(ctx) {
 			@Override
 			protected void onTextChanged(CharSequence text, int start,
 					int lengthBefore, int lengthAfter) {
@@ -68,9 +61,9 @@ OnEditorActionListener {
 		nicknameEntry.setMaxEms(20);
 		nicknameEntry.setMaxLines(1);
 		nicknameEntry.setOnEditorActionListener(this);
-		layout.addView(nicknameEntry);
+		addView(nicknameEntry);
 
-		innerLayout = new LinearLayout(this);
+		innerLayout = new LinearLayout(ctx);
 		innerLayout.setOrientation(HORIZONTAL);
 		innerLayout.setGravity(CENTER);
 
@@ -83,20 +76,16 @@ OnEditorActionListener {
 		done.setEnabled(false);
 		done.setOnClickListener(this);
 		innerLayout.addView(done);
-		layout.addView(innerLayout);
-
-		setContentView(layout);
+		addView(innerLayout);
 	}
 
 	public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
-		if(textView.getText().length() > 0) finish();
+		if(textView.getText().length() > 0) container.finish();
 		return true;
 	}
 
 	public void onClick(View view) {
-		if(done == null) return;
-		if(view != done)
-			startActivity(new Intent(this, NetworkSetupActivity.class));
-		finish();
+		if(view == done) container.finish(); // Done
+		else container.setView(new NetworkSetupView(container)); // Add another
 	}
 }
diff --git a/src/net/sf/briar/android/invitation/InvitationCodeActivity.java b/src/net/sf/briar/android/invitation/InvitationCodeActivity.java
deleted file mode 100644
index 8f3bb1b41b5df7df5a286ade4970c13a86803db1..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/InvitationCodeActivity.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import roboguice.activity.RoboActivity;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.google.inject.Inject;
-
-public class InvitationCodeActivity extends RoboActivity
-implements CodeEntryListener {
-
-	@Inject private InvitationManager manager;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		TextView yourCode = new TextView(this);
-		yourCode.setGravity(CENTER_HORIZONTAL);
-		yourCode.setText(R.string.your_invitation_code);
-		layout.addView(yourCode);
-
-		TextView code = new TextView(this);
-		code.setGravity(CENTER_HORIZONTAL);
-		code.setTextSize(50);
-		code.setText(manager.getLocalInvitationCode());
-		layout.addView(code);
-
-		CodeEntryWidget codeEntry = new CodeEntryWidget(this);
-		Resources res = getResources();
-		codeEntry.init(this, res.getString(R.string.enter_invitation_code));
-		layout.addView(codeEntry);
-
-		setContentView(layout);
-	}
-
-	public void codeEntered(String code) {
-		manager.setRemoteInvitationCode(code);
-		Intent intent = new Intent(this, ConnectionActivity.class);
-		intent.putExtras(getIntent().getExtras());
-		startActivity(intent);
-		finish();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/InvitationCodeView.java b/src/net/sf/briar/android/invitation/InvitationCodeView.java
new file mode 100644
index 0000000000000000000000000000000000000000..fae6cca52830152b72e1e008b5698fcf10fcf52d
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/InvitationCodeView.java
@@ -0,0 +1,46 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.content.Context;
+import android.content.res.Resources;
+import android.widget.TextView;
+
+public class InvitationCodeView extends AddContactView
+implements CodeEntryListener {
+
+	private int localCode = -1;
+
+	InvitationCodeView(Context ctx) {
+		super(ctx);
+	}
+
+	void init(AddContactActivity container) {
+		localCode = container.generateLocalInvitationCode();
+		super.init(container);
+	}
+
+	void populate() {
+		removeAllViews();
+		Context ctx = getContext();
+		TextView yourCode = new TextView(ctx);
+		yourCode.setGravity(CENTER_HORIZONTAL);
+		yourCode.setText(R.string.your_invitation_code);
+		addView(yourCode);
+
+		TextView code = new TextView(ctx);
+		code.setGravity(CENTER_HORIZONTAL);
+		code.setTextSize(50);
+		code.setText(String.format("%06d", localCode));
+		addView(code);
+
+		CodeEntryWidget codeEntry = new CodeEntryWidget(ctx);
+		Resources res = getResources();
+		codeEntry.init(this, res.getString(R.string.enter_invitation_code));
+		addView(codeEntry);
+	}
+
+	public void codeEntered(int remoteCode) {
+		container.remoteInvitationCodeEntered(remoteCode);
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/InvitationManager.java b/src/net/sf/briar/android/invitation/InvitationManager.java
deleted file mode 100644
index f77baa4c33aa76824a0603eb6eb7e52cde5682b7..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/InvitationManager.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import android.content.Context;
-
-interface InvitationManager {
-
-	int TIMEOUT = 20 * 1000;
-
-	void tryToConnect(ConnectionListener listener);
-
-	String getLocalInvitationCode();
-
-	String getRemoteInvitationCode();
-
-	void setRemoteInvitationCode(String code);
-
-	void startWifiConnectionWorker(Context ctx);
-
-	void startBluetoothConnectionWorker(Context ctx);
-
-	String getLocalConfirmationCode();
-
-	String getRemoteConfirmationCode();
-
-	void startConfirmationWorker(ConfirmationListener listener);
-}
diff --git a/src/net/sf/briar/android/invitation/InvitationManagerImpl.java b/src/net/sf/briar/android/invitation/InvitationManagerImpl.java
deleted file mode 100644
index c5afcacec161d9bd116b7846a8bd8aa61640ef96..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/InvitationManagerImpl.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import android.content.Context;
-import android.util.Log;
-
-class InvitationManagerImpl implements InvitationManager {
-
-	public void tryToConnect(final ConnectionListener listener) {
-		new Thread() {
-			@Override
-			public void run() {
-				try {
-					// FIXME
-					Thread.sleep((long) (Math.random() * TIMEOUT));
-					if(Math.random() < 0.5) listener.connectionEstablished();
-					else listener.connectionNotEstablished();
-				} catch(InterruptedException e) {
-					Log.w(getClass().getName(), e.toString());
-					listener.connectionNotEstablished();
-				}
-			}
-		}.start();
-	}
-
-	public String getLocalInvitationCode() {
-		// FIXME
-		return "123456";
-	}
-
-	public String getRemoteInvitationCode() {
-		// FIXME
-		return "123456";
-	}
-
-	public void setRemoteInvitationCode(String code) {
-		// FIXME
-	}
-
-	public void startWifiConnectionWorker(Context ctx) {
-		// FIXME
-	}
-
-	public void startBluetoothConnectionWorker(Context ctx) {
-		// FIXME
-	}
-
-	public String getLocalConfirmationCode() {
-		// FIXME
-		return "123456";
-	}
-
-	public String getRemoteConfirmationCode() {
-		// FIXME
-		return "123456";
-	}
-
-	public void startConfirmationWorker(ConfirmationListener listener) {
-		// FIXME
-		try {
-			Thread.sleep(1000 + (int) (Math.random() * 4 * 1000));
-		} catch(InterruptedException e) {
-			Log.w(getClass().getName(), e.toString());
-			Thread.currentThread().interrupt();
-		}
-		listener.confirmationReceived();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/NetworkSetupActivity.java b/src/net/sf/briar/android/invitation/NetworkSetupActivity.java
deleted file mode 100644
index abe9065dbbbefc6f4ca22995e852936f34cd9ff6..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/NetworkSetupActivity.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-public class NetworkSetupActivity extends Activity
-implements WifiStateListener, BluetoothStateListener, OnClickListener {
-
-	private WifiWidget wifi = null;
-	private BluetoothWidget bluetooth = null;
-	private Button continueButton = null;
-	private String networkName = null;
-	private boolean useBluetooth = false;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		TextView sameNetwork = new TextView(this);
-		sameNetwork.setGravity(CENTER_HORIZONTAL);
-		sameNetwork.setText(R.string.same_network);
-		layout.addView(sameNetwork);
-
-		wifi = new WifiWidget(this);
-		wifi.init(this);
-		layout.addView(wifi);
-
-		bluetooth = new BluetoothWidget(this);
-		bluetooth.init(this);
-		layout.addView(bluetooth);
-
-		continueButton = new Button(this);
-		LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
-		continueButton.setLayoutParams(lp);
-		continueButton.setText(R.string.continue_button);
-		continueButton.setOnClickListener(this);
-		enableOrDisableContinueButton();
-		layout.addView(continueButton);
-
-		setContentView(layout);
-	}
-
-	@Override
-	public void onResume() {
-		super.onResume();
-		wifi.populate();
-		bluetooth.populate();
-	}
-
-	public void wifiStateChanged(final String name) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				networkName = name;
-				enableOrDisableContinueButton();
-			}
-		});
-	}
-
-	public void bluetoothStateChanged(final boolean enabled) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				useBluetooth = enabled;
-				enableOrDisableContinueButton();
-			}
-		});
-	}
-
-	private void enableOrDisableContinueButton() {
-		if(continueButton == null) return; // Activity not created yet
-		if(useBluetooth || networkName != null) continueButton.setEnabled(true);
-		else continueButton.setEnabled(false);
-	}
-
-	public void onClick(View view) {
-		Intent intent = new Intent(this, InvitationCodeActivity.class);
-		intent.putExtra("net.sf.briar.android.invitation.NETWORK_NAME",
-				networkName);
-		intent.putExtra("net.sf.briar.android.invitation.USE_BLUETOOTH",
-				useBluetooth);
-		startActivity(intent);
-		finish();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/NetworkSetupView.java b/src/net/sf/briar/android/invitation/NetworkSetupView.java
new file mode 100644
index 0000000000000000000000000000000000000000..da4d7671eade24d1ef5608c368239be434feaf32
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/NetworkSetupView.java
@@ -0,0 +1,76 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import net.sf.briar.R;
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class NetworkSetupView extends AddContactView
+implements WifiStateListener, BluetoothStateListener, OnClickListener {
+
+	private Button continueButton = null;
+
+	NetworkSetupView(Context ctx) {
+		super(ctx);
+	}
+
+	void populate() {
+		removeAllViews();
+		Context ctx = getContext();
+		TextView sameNetwork = new TextView(ctx);
+		sameNetwork.setGravity(CENTER_HORIZONTAL);
+		sameNetwork.setText(R.string.same_network);
+		addView(sameNetwork);
+
+		WifiWidget wifi = new WifiWidget(ctx);
+		wifi.init(this);
+		addView(wifi);
+
+		BluetoothWidget bluetooth = new BluetoothWidget(ctx);
+		bluetooth.init(this);
+		addView(bluetooth);
+
+		continueButton = new Button(ctx);
+		LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+		continueButton.setLayoutParams(lp);
+		continueButton.setText(R.string.continue_button);
+		continueButton.setOnClickListener(this);
+		enableOrDisableContinueButton();
+		addView(continueButton);
+	}
+
+	public void wifiStateChanged(final String networkName) {
+		container.runOnUiThread(new Runnable() {
+			public void run() {
+				container.setNetworkName(networkName);
+				enableOrDisableContinueButton();
+			}
+		});
+	}
+
+	public void bluetoothStateChanged(final boolean enabled) {
+		container.runOnUiThread(new Runnable() {
+			public void run() {
+				container.setUseBluetooth(enabled);
+				enableOrDisableContinueButton();
+			}
+		});
+	}
+
+	private void enableOrDisableContinueButton() {
+		if(continueButton == null) return; // Activity not created yet
+		boolean useBluetooth = container.getUseBluetooth();
+		String networkName = container.getNetworkName();
+		if(useBluetooth || networkName != null) continueButton.setEnabled(true);
+		else continueButton.setEnabled(false);
+	}
+
+	public void onClick(View view) {
+		// Continue
+		container.setView(new InvitationCodeView(container));
+	}
+}
diff --git a/src/net/sf/briar/android/invitation/WaitForContactActivity.java b/src/net/sf/briar/android/invitation/WaitForContactActivity.java
deleted file mode 100644
index 7d93aea62d9bf78387d3e001c4294487ac42e821..0000000000000000000000000000000000000000
--- a/src/net/sf/briar/android/invitation/WaitForContactActivity.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package net.sf.briar.android.invitation;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.LinearLayout.VERTICAL;
-import net.sf.briar.R;
-import roboguice.activity.RoboActivity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import com.google.inject.Inject;
-
-public class WaitForContactActivity extends RoboActivity
-implements ConfirmationListener {
-
-	@Inject private InvitationManager manager;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		LinearLayout innerLayout = new LinearLayout(this);
-		innerLayout.setOrientation(HORIZONTAL);
-		innerLayout.setGravity(CENTER);
-
-		ImageView icon = new ImageView(this);
-		icon.setPadding(10, 10, 10, 10);
-		icon.setImageResource(R.drawable.navigation_accept);
-		innerLayout.addView(icon);
-
-		TextView failed = new TextView(this);
-		failed.setTextSize(20);
-		failed.setText(R.string.connected_to_contact);
-		innerLayout.addView(failed);
-		layout.addView(innerLayout);
-
-		TextView yourCode = new TextView(this);
-		yourCode.setGravity(CENTER_HORIZONTAL);
-		yourCode.setText(R.string.your_confirmation_code);
-		layout.addView(yourCode);
-
-		TextView code = new TextView(this);
-		code.setGravity(CENTER_HORIZONTAL);
-		code.setTextSize(50);
-		code.setText(manager.getLocalConfirmationCode());
-		layout.addView(code);
-
-		innerLayout = new LinearLayout(this);
-		innerLayout.setOrientation(HORIZONTAL);
-		innerLayout.setGravity(CENTER);
-
-		ProgressBar progress = new ProgressBar(this);
-		progress.setIndeterminate(true);
-		progress.setPadding(0, 10, 10, 0);
-		innerLayout.addView(progress);
-
-		TextView connecting = new TextView(this);
-		connecting.setText(R.string.waiting_for_contact);
-		innerLayout.addView(connecting);
-		layout.addView(innerLayout);
-
-		setContentView(layout);
-
-		manager.startConfirmationWorker(this);
-	}
-
-	public void confirmationReceived() {
-		startActivity(new Intent(this, ContactAddedActivity.class));
-		finish();
-	}
-
-	public void confirmationNotReceived() {
-		Intent intent = new Intent(this, CodesDoNotMatchActivity.class);
-		intent.putExtras(getIntent().getExtras());
-		startActivity(intent);
-		finish();
-	}
-}
diff --git a/src/net/sf/briar/android/invitation/WaitForContactView.java b/src/net/sf/briar/android/invitation/WaitForContactView.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7c5824cb6dce35179bf7b69ec929777379e09f1
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/WaitForContactView.java
@@ -0,0 +1,62 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.content.Context;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class WaitForContactView extends AddContactView {
+
+	WaitForContactView(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.setPadding(10, 10, 10, 10);
+		icon.setImageResource(R.drawable.navigation_accept);
+		innerLayout.addView(icon);
+
+		TextView failed = new TextView(ctx);
+		failed.setTextSize(20);
+		failed.setText(R.string.connected_to_contact);
+		innerLayout.addView(failed);
+		addView(innerLayout);
+
+		TextView yourCode = new TextView(ctx);
+		yourCode.setGravity(CENTER_HORIZONTAL);
+		yourCode.setText(R.string.your_confirmation_code);
+		addView(yourCode);
+
+		TextView code = new TextView(ctx);
+		code.setGravity(CENTER_HORIZONTAL);
+		code.setTextSize(50);
+		int localCode = container.getLocalConfirmationCode();
+		code.setText(String.format("%06d", localCode));
+		addView(code);
+
+		innerLayout = new LinearLayout(ctx);
+		innerLayout.setOrientation(HORIZONTAL);
+		innerLayout.setGravity(CENTER);
+
+		ProgressBar progress = new ProgressBar(ctx);
+		progress.setIndeterminate(true);
+		progress.setPadding(0, 10, 10, 0);
+		innerLayout.addView(progress);
+
+		TextView connecting = new TextView(ctx);
+		connecting.setText(R.string.waiting_for_contact);
+		innerLayout.addView(connecting);
+		addView(innerLayout);
+	}
+}
diff --git a/src/net/sf/briar/api/crypto/CryptoComponent.java b/src/net/sf/briar/api/crypto/CryptoComponent.java
index c6e4b34fd9fc52d7439e819cdd58367eb906aa43..ae63dfa3d24b13c75c8a8ae6ce423e011edb856a 100644
--- a/src/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/src/net/sf/briar/api/crypto/CryptoComponent.java
@@ -11,7 +11,7 @@ public interface CryptoComponent {
 
 	/**
 	 * Derives a tag key from the given temporary secret.
-	 * @param alice Indicates whether the key is for connections initiated by
+	 * @param alice indicates whether the key is for connections initiated by
 	 * Alice or Bob.
 	 */
 	ErasableKey deriveTagKey(byte[] secret, boolean alice);
@@ -19,9 +19,9 @@ public interface CryptoComponent {
 	/**
 	 * Derives a frame key from the given temporary secret and connection
 	 * number.
-	 * @param alice Indicates whether the key is for a connection initiated by
+	 * @param alice indicates whether the key is for a connection initiated by
 	 * Alice or Bob.
-	 * @param initiator Indicates whether the key is for the initiator's or the
+	 * @param initiator indicates whether the key is for the initiator's or the
 	 * responder's side of the connection.
 	 */
 	ErasableKey deriveFrameKey(byte[] secret, long connection, boolean alice,
@@ -30,7 +30,7 @@ public interface CryptoComponent {
 	/**
 	 * Derives an initial shared secret from two public keys and one of the
 	 * corresponding private keys.
-	 * @param alice Indicates whether the private key belongs to Alice or Bob.
+	 * @param alice indicates whether the private key belongs to Alice or Bob.
 	 */
 	byte[] deriveInitialSecret(byte[] ourPublicKey, byte[] theirPublicKey,
 			PrivateKey ourPrivateKey, boolean alice);
@@ -67,7 +67,7 @@ public interface CryptoComponent {
 
 	MessageDigest getMessageDigest();
 
-	PseudoRandom getPseudoRandom(int seed);
+	PseudoRandom getPseudoRandom(int seed1, int seed2);
 
 	SecureRandom getSecureRandom();
 
diff --git a/src/net/sf/briar/api/db/DatabaseComponent.java b/src/net/sf/briar/api/db/DatabaseComponent.java
index cc6ae698fa1b69492c00e46d8071837a6797454e..b584830233a58415fbd91176ed4442dcc2313a99 100644
--- a/src/net/sf/briar/api/db/DatabaseComponent.java
+++ b/src/net/sf/briar/api/db/DatabaseComponent.java
@@ -34,7 +34,7 @@ public interface DatabaseComponent {
 
 	/**
 	 * Opens the database.
-	 * @param resume True to reopen an existing database or false to create a
+	 * @param resume true to reopen an existing database or false to create a
 	 * new one.
 	 */
 	void open(boolean resume) throws DbException, IOException;
diff --git a/src/net/sf/briar/api/invitation/ConfirmationCallback.java b/src/net/sf/briar/api/invitation/ConfirmationCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..652773f863a40cd3588e1af8112e27848161b781
--- /dev/null
+++ b/src/net/sf/briar/api/invitation/ConfirmationCallback.java
@@ -0,0 +1,14 @@
+package net.sf.briar.api.invitation;
+
+/** An interface for informing a peer of whether confirmation codes match. */
+public interface ConfirmationCallback {
+
+	/**  Called to indicate that the confirmation codes match. */
+	void codesMatch();
+
+	/**
+	 * Called to indicate that either the confirmation codes do not match or
+	 * the result of the comparison is unknown.
+	 */
+	void codesDoNotMatch();
+}
diff --git a/src/net/sf/briar/api/invitation/ConnectionCallback.java b/src/net/sf/briar/api/invitation/ConnectionCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..56ce5c73bfb1064a7d07d4f203741614f0c33f77
--- /dev/null
+++ b/src/net/sf/briar/api/invitation/ConnectionCallback.java
@@ -0,0 +1,18 @@
+package net.sf.briar.api.invitation;
+
+/** An interface for monitoring the status of an invitation connection. */
+public interface ConnectionCallback extends ConfirmationCallback {
+
+	/**
+	 * Called if the connection is successfully established.
+	 * @param localCode the local confirmation code.
+	 * @param remoteCode the remote confirmation code.
+	 * @param c a callback to inform the remote peer of the result of the local
+	 * peer's confirmation code comparison.
+	 */
+	void connectionEstablished(int localCode, int remoteCode,
+			ConfirmationCallback c);
+
+	/** Called if the connection cannot be established. */
+	void connectionNotEstablished();
+}
diff --git a/src/net/sf/briar/api/invitation/InvitationManager.java b/src/net/sf/briar/api/invitation/InvitationManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0ae1774ceb27f345ca5cda0ff737800823803eb
--- /dev/null
+++ b/src/net/sf/briar/api/invitation/InvitationManager.java
@@ -0,0 +1,17 @@
+package net.sf.briar.api.invitation;
+
+/**
+ * Allows invitation connections to be established and their status to be
+ * monitored.
+ */
+public interface InvitationManager {
+
+	/**
+	 * Tries to establish an invitation connection.
+	 * @param localCode the local invitation code.
+	 * @param remoteCode the remote invitation code.
+	 * @param c1 a callback to be informed of the connection's status and the
+	 * result of the remote peer's confirmation code comparison.
+	 */
+	void connect(int localCode, int remoteCode, ConnectionCallback c);
+}
diff --git a/src/net/sf/briar/api/plugins/InvitationConstants.java b/src/net/sf/briar/api/plugins/InvitationConstants.java
index 6941ad0e0ec78b43c27908f7413e3f3fbfb08b2c..f27d35b825690f62a5d3e79c7231ddaf6e104dcf 100644
--- a/src/net/sf/briar/api/plugins/InvitationConstants.java
+++ b/src/net/sf/briar/api/plugins/InvitationConstants.java
@@ -6,9 +6,9 @@ public interface InvitationConstants {
 
 	int CODE_BITS = 19; // Codes must fit into six decimal digits
 
-	int MAX_CODE = 1 << CODE_BITS - 1;
+	int MAX_CODE = (1 << CODE_BITS) - 1;
 
-	int HASH_LENGTH = 48;
+	int HASH_LENGTH = 48; // Bytes
 
-	int MAX_PUBLIC_KEY_LENGTH = 120;
+	int MAX_PUBLIC_KEY_LENGTH = 120; // Bytes
 }
diff --git a/src/net/sf/briar/api/plugins/Plugin.java b/src/net/sf/briar/api/plugins/Plugin.java
index 319f58148b070124a911b119258bec5db2885aa5..4debef1e2984059d4fd8d3d3f054e16112b9c962 100644
--- a/src/net/sf/briar/api/plugins/Plugin.java
+++ b/src/net/sf/briar/api/plugins/Plugin.java
@@ -21,14 +21,14 @@ public interface Plugin {
 	void stop() throws IOException;
 
 	/**
-	 * Returns true if the plugin's poll() method should be called
-	 * periodically to attempt to establish connections.
+	 * Returns true if the plugin's {@link Plugin#poll(Collection)} method
+	 * should be called periodically to attempt to establish connections.
 	 */
 	boolean shouldPoll();
 
 	/**
 	 * Returns the desired interval in milliseconds between calls to the
-	 * plugin's poll() method.
+	 * plugin's {@link Plugin#poll(Collection)} method.
 	 */
 	long getPollingInterval();
 
diff --git a/src/net/sf/briar/api/plugins/PluginCallback.java b/src/net/sf/briar/api/plugins/PluginCallback.java
index 63435f9f50abe2be326e20489a6beb3c7be8e8b2..895d4a8b253f78338d922baefb4511b011de2c3f 100644
--- a/src/net/sf/briar/api/plugins/PluginCallback.java
+++ b/src/net/sf/briar/api/plugins/PluginCallback.java
@@ -33,7 +33,7 @@ public interface PluginCallback {
 	 * Presents the user with a choice among two or more named options and
 	 * returns the user's response. The message may consist of a translatable
 	 * format string and arguments.
-	 * @return An index into the array of options indicating the user's choice,
+	 * @return an index into the array of options indicating the user's choice,
 	 * or -1 if the user cancelled the choice.
 	 */
 	int showChoice(String[] options, String... message);
diff --git a/src/net/sf/briar/api/plugins/PluginManager.java b/src/net/sf/briar/api/plugins/PluginManager.java
index f3f49ab716a5f9525111da1218bfe2e511e725b8..77bc1dc982fea2c6fc1f224c4e1e2342fbf72f61 100644
--- a/src/net/sf/briar/api/plugins/PluginManager.java
+++ b/src/net/sf/briar/api/plugins/PluginManager.java
@@ -5,6 +5,10 @@ import java.util.Collection;
 import net.sf.briar.api.plugins.duplex.DuplexPlugin;
 import android.content.Context;
 
+/**
+ * Responsible for starting transport plugins at startup, stopping them at
+ * shutdown, and providing access to plugins for exchanging invitations.
+ */
 public interface PluginManager {
 
 	/**
diff --git a/src/net/sf/briar/api/plugins/simplex/SimplexTransportReader.java b/src/net/sf/briar/api/plugins/simplex/SimplexTransportReader.java
index b7f0f35ed956fc6ea59cf26c226ccad84fc3e191..ffda7ef0d50df74efe0ad739e62d7f4a2fdb2400 100644
--- a/src/net/sf/briar/api/plugins/simplex/SimplexTransportReader.java
+++ b/src/net/sf/briar/api/plugins/simplex/SimplexTransportReader.java
@@ -9,8 +9,7 @@ import java.io.InputStream;
  */
 public interface SimplexTransportReader {
 
-	/** Returns an input stream for reading from the transport. 
-	 * @throws IOException */
+	/** Returns an input stream for reading from the transport. */
 	InputStream getInputStream() throws IOException;
 
 	/**
diff --git a/src/net/sf/briar/api/plugins/simplex/SimplexTransportWriter.java b/src/net/sf/briar/api/plugins/simplex/SimplexTransportWriter.java
index c48b2795de6be837fd917777884f0d65712198d2..152b7fd673e9d0402c5875007b49d2df90590dd9 100644
--- a/src/net/sf/briar/api/plugins/simplex/SimplexTransportWriter.java
+++ b/src/net/sf/briar/api/plugins/simplex/SimplexTransportWriter.java
@@ -12,8 +12,7 @@ public interface SimplexTransportWriter {
 	/** Returns the capacity of the transport in bytes. */
 	long getCapacity();
 
-	/** Returns an output stream for writing to the transport. 
-	 * @throws IOException */
+	/** Returns an output stream for writing to the transport. */
 	OutputStream getOutputStream() throws IOException;
 
 	/**
diff --git a/src/net/sf/briar/api/ui/UiCallback.java b/src/net/sf/briar/api/ui/UiCallback.java
index d241eadb372ca52d0402de36d78d024a709ac425..4903ef89e5a013857088cb5f2f29a58fd40be4eb 100644
--- a/src/net/sf/briar/api/ui/UiCallback.java
+++ b/src/net/sf/briar/api/ui/UiCallback.java
@@ -6,7 +6,7 @@ public interface UiCallback {
 	 * Presents the user with a choice among two or more named options and
 	 * returns the user's response. The message may consist of a translatable
 	 * format string and arguments.
-	 * @return An index into the array of options indicating the user's choice,
+	 * @return an index into the array of options indicating the user's choice,
 	 * or -1 if the user cancelled the choice.
 	 */
 	int showChoice(String[] options, String... message);
diff --git a/src/net/sf/briar/crypto/CryptoComponentImpl.java b/src/net/sf/briar/crypto/CryptoComponentImpl.java
index bc1c07a53f6ead71c77c1c98632e0619a443a921..7ac9d230ec3c5854326db3f45e4ec5857903e221 100644
--- a/src/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/src/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -274,8 +274,8 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
-	public PseudoRandom getPseudoRandom(int seed) {
-		return new PseudoRandomImpl(getMessageDigest(), seed);
+	public PseudoRandom getPseudoRandom(int seed1, int seed2) {
+		return new PseudoRandomImpl(getMessageDigest(), seed1, seed2);
 	}
 
 	public SecureRandom getSecureRandom() {
diff --git a/src/net/sf/briar/crypto/PseudoRandomImpl.java b/src/net/sf/briar/crypto/PseudoRandomImpl.java
index 1f74586e4a829e9e58cc117ec3132c1b07dcd5fc..c2751cf25db2a7b88f19da7b70a7bf3f3767c684 100644
--- a/src/net/sf/briar/crypto/PseudoRandomImpl.java
+++ b/src/net/sf/briar/crypto/PseudoRandomImpl.java
@@ -11,10 +11,11 @@ class PseudoRandomImpl implements PseudoRandom {
 	private byte[] state;
 	private int offset;
 
-	PseudoRandomImpl(MessageDigest messageDigest, int seed) {
+	PseudoRandomImpl(MessageDigest messageDigest, int seed1, int seed2) {
 		this.messageDigest = messageDigest;
-		byte[] seedBytes = new byte[4];
-		ByteUtils.writeUint32(seed, seedBytes, 0);
+		byte[] seedBytes = new byte[8];
+		ByteUtils.writeUint32(seed1, seedBytes, 0);
+		ByteUtils.writeUint32(seed2, seedBytes, 4);
 		messageDigest.update(seedBytes);
 		state = messageDigest.digest();
 		offset = 0;
diff --git a/src/net/sf/briar/db/Database.java b/src/net/sf/briar/db/Database.java
index 5303fae3dc7f80cc3161f30b9f53b64863304e26..3959e735d1229ebe32ce102bdd514f64ba572705 100644
--- a/src/net/sf/briar/db/Database.java
+++ b/src/net/sf/briar/db/Database.java
@@ -24,9 +24,9 @@ import net.sf.briar.api.transport.TemporarySecret;
 /**
  * A low-level interface to the database (DatabaseComponent provides a
  * high-level interface). Most operations take a transaction argument, which is
- * obtained by calling startTransaction(). Every transaction must be
- * terminated by calling either abortTransaction() or commitTransaction(),
- * even if an exception is thrown.
+ * obtained by calling {@link #startTransaction()}. Every transaction must be
+ * terminated by calling either {@link #abortTransaction(T)} or
+ * {@link #commitTransaction(T)}, even if an exception is thrown.
  * <p>
  * Locking is provided by the DatabaseComponent implementation. To prevent
  * deadlock, locks must be acquired in the following order:
@@ -45,7 +45,7 @@ interface Database<T> {
 
 	/**
 	 * Opens the database.
-	 * @param resume True to reopen an existing database, false to create a
+	 * @param resume true to reopen an existing database, false to create a
 	 * new one.
 	 */
 	void open(boolean resume) throws DbException, IOException;
diff --git a/src/net/sf/briar/db/DatabaseComponentImpl.java b/src/net/sf/briar/db/DatabaseComponentImpl.java
index 147226633fe2fb0ca71672fabef34e4118444635..84d63e81fd128a5c20a65ef137a0e5b130e144db 100644
--- a/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -326,7 +326,7 @@ DatabaseCleaner.Callback {
 	 * that have changed from sendable to not sendable, or vice versa.
 	 * <p>
 	 * Locking: message write.
-	 * @param increment True if the message's sendability has changed from 0 to
+	 * @param increment true if the message's sendability has changed from 0 to
 	 * greater than 0, or false if it has changed from greater than 0 to 0.
 	 */
 	private int updateAncestorSendability(T txn, MessageId m, boolean increment)
@@ -1396,7 +1396,7 @@ DatabaseCleaner.Callback {
 	 * the ancestors of those messages if necessary.
 	 * <p>
 	 * Locking: message write.
-	 * @param increment True if the user's rating for the author has changed
+	 * @param increment true if the user's rating for the author has changed
 	 * from not good to good, or false if it has changed from good to not good.
 	 */
 	private void updateAuthorSendability(T txn, AuthorId a, boolean increment)
diff --git a/src/net/sf/briar/invitation/InvitationManagerImpl.java b/src/net/sf/briar/invitation/InvitationManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d4967a068d858b697e5383724c16d38b75060b0
--- /dev/null
+++ b/src/net/sf/briar/invitation/InvitationManagerImpl.java
@@ -0,0 +1,81 @@
+package net.sf.briar.invitation;
+
+import java.util.Collection;
+
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.crypto.PseudoRandom;
+import net.sf.briar.api.invitation.ConfirmationCallback;
+import net.sf.briar.api.invitation.ConnectionCallback;
+import net.sf.briar.api.invitation.InvitationManager;
+import net.sf.briar.api.plugins.PluginManager;
+import net.sf.briar.api.plugins.duplex.DuplexPlugin;
+
+import com.google.inject.Inject;
+
+class InvitationManagerImpl implements InvitationManager {
+
+	private final CryptoComponent crypto;
+	private final PluginManager pluginManager;
+
+	@Inject
+	InvitationManagerImpl(CryptoComponent crypto, PluginManager pluginManager) {
+		this.crypto = crypto;
+		this.pluginManager = pluginManager;
+	}
+
+	public void connect(int localCode, int remoteCode, ConnectionCallback c) {
+		Collection<DuplexPlugin> plugins = pluginManager.getInvitationPlugins();
+		// Alice is the party with the smaller invitation code
+		if(localCode < remoteCode) {
+			PseudoRandom r = crypto.getPseudoRandom(localCode, remoteCode);
+			startAliceInvitationWorker(plugins, r, c);
+		} else {
+			PseudoRandom r = crypto.getPseudoRandom(remoteCode, localCode);
+			startBobInvitationWorker(plugins, r, c);
+		}
+	}
+
+	private void startAliceInvitationWorker(Collection<DuplexPlugin> plugins,
+			PseudoRandom r, ConnectionCallback c) {
+		// FIXME
+		new FakeWorkerThread(c).start();
+	}
+
+	private void startBobInvitationWorker(Collection<DuplexPlugin> plugins,
+			PseudoRandom r, ConnectionCallback c) {
+		// FIXME
+		new FakeWorkerThread(c).start();
+	}
+
+	private static class FakeWorkerThread extends Thread {
+
+		private final ConnectionCallback callback;
+
+		private FakeWorkerThread(ConnectionCallback callback) {
+			this.callback = callback;
+		}
+
+		@Override
+		public void run() {
+			try {
+				Thread.sleep((long) (Math.random() * 30 * 1000));
+			} catch(InterruptedException ignored) {}
+			if(Math.random() < 0.8) {
+				callback.connectionNotEstablished();
+			} else {
+				callback.connectionEstablished(123456, 123456,
+						new ConfirmationCallback() {
+
+					public void codesMatch() {}
+
+					public void codesDoNotMatch() {}
+				});
+				try {
+					Thread.sleep((long) (Math.random() * 10 * 1000));
+				} catch(InterruptedException ignored) {}
+				if(Math.random() < 0.5) callback.codesMatch();
+				else callback.codesDoNotMatch();
+			}
+		}
+	}
+}
diff --git a/src/net/sf/briar/invitation/InvitationModule.java b/src/net/sf/briar/invitation/InvitationModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..41197dc7fcff95c9671ccbb12301991394353656
--- /dev/null
+++ b/src/net/sf/briar/invitation/InvitationModule.java
@@ -0,0 +1,13 @@
+package net.sf.briar.invitation;
+
+import net.sf.briar.api.invitation.InvitationManager;
+
+import com.google.inject.AbstractModule;
+
+public class InvitationModule extends AbstractModule {
+
+	@Override
+	protected void configure() {
+		bind(InvitationManager.class).to(InvitationManagerImpl.class);
+	}
+}
diff --git a/src/net/sf/briar/plugins/PluginManagerImpl.java b/src/net/sf/briar/plugins/PluginManagerImpl.java
index 4fb27488951e60e72a435409965d079fb1c14896..110e201c0248542b3c326144d1599f7e1e379c54 100644
--- a/src/net/sf/briar/plugins/PluginManagerImpl.java
+++ b/src/net/sf/briar/plugins/PluginManagerImpl.java
@@ -95,6 +95,7 @@ class PluginManagerImpl implements PluginManager {
 	public synchronized int start(Context appContext) {
 		Set<TransportId> ids = new HashSet<TransportId>();
 		// Instantiate and start the simplex plugins
+		if(LOG.isLoggable(INFO)) LOG.info("Starting simplex plugins");
 		for(String s : getSimplexPluginFactoryNames()) {
 			try {
 				Class<?> c = Class.forName(s);
@@ -128,6 +129,7 @@ class PluginManagerImpl implements PluginManager {
 			}
 		}
 		// Instantiate and start the duplex plugins
+		if(LOG.isLoggable(INFO)) LOG.info("Starting duplex plugins");
 		for(String s : getDuplexPluginFactoryNames()) {
 			try {
 				Class<?> c = Class.forName(s);
@@ -161,6 +163,7 @@ class PluginManagerImpl implements PluginManager {
 			}
 		}
 		// Start the poller
+		if(LOG.isLoggable(INFO)) LOG.info("Starting poller");
 		List<Plugin> plugins = new ArrayList<Plugin>();
 		plugins.addAll(simplexPlugins);
 		plugins.addAll(duplexPlugins);
@@ -182,8 +185,10 @@ class PluginManagerImpl implements PluginManager {
 	public synchronized int stop() {
 		int stopped = 0;
 		// Stop the poller
+		if(LOG.isLoggable(INFO)) LOG.info("Stopping poller");
 		poller.stop();
 		// Stop the simplex plugins
+		if(LOG.isLoggable(INFO)) LOG.info("Stopping simplex plugins");
 		for(SimplexPlugin plugin : simplexPlugins) {
 			try {
 				plugin.stop();
@@ -192,7 +197,9 @@ class PluginManagerImpl implements PluginManager {
 				if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
 			}
 		}
+		simplexPlugins.clear();
 		// Stop the duplex plugins
+		if(LOG.isLoggable(INFO)) LOG.info("Stopping duplex plugins");
 		for(DuplexPlugin plugin : duplexPlugins) {
 			try {
 				plugin.stop();
@@ -201,7 +208,9 @@ class PluginManagerImpl implements PluginManager {
 				if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
 			}
 		}
+		duplexPlugins.clear();
 		// Shut down the executors
+		if(LOG.isLoggable(INFO)) LOG.info("Stopping executors");
 		pluginExecutor.shutdown();
 		androidExecutor.shutdown();
 		// Return the number of plugins successfully stopped
diff --git a/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java b/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java
index 1c8b196c88d961ed8f7979e7e01721a7808505a3..bfd543d06f91677f21c4725c3a5cc7713ffb6f22 100644
--- a/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java
+++ b/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java
@@ -349,10 +349,10 @@ class DroidtoothPlugin implements DuplexPlugin {
 			if(!running) return;
 		}
 		if(adapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) return;
-		Intent intent = new Intent(ACTION_REQUEST_DISCOVERABLE);
-		intent.putExtra(EXTRA_DISCOVERABLE_DURATION, 60);
-		intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
-		appContext.startActivity(intent);
+		Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
+		i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
+		i.addFlags(FLAG_ACTIVITY_NEW_TASK);
+		appContext.startActivity(i);
 	}
 
 	private static class BluetoothStateReceiver extends BroadcastReceiver {
diff --git a/src/net/sf/briar/transport/ConnectionWriterImpl.java b/src/net/sf/briar/transport/ConnectionWriterImpl.java
index d27905938ce0535df1d77f8b13efa2d87eb3315c..4d1b2c571a79bb756c8c6e32a52de70ee5fe376e 100644
--- a/src/net/sf/briar/transport/ConnectionWriterImpl.java
+++ b/src/net/sf/briar/transport/ConnectionWriterImpl.java
@@ -11,7 +11,7 @@ import net.sf.briar.api.transport.ConnectionWriter;
 
 /**
  * A ConnectionWriter that buffers its input and writes a frame whenever there
- * is a full frame to write or the flush() method is called.
+ * is a full frame to write or the {@link #flush()} method is called.
  * <p>
  * This class is not thread-safe.
  */