diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index ab787cf3790bb65471200edaac706240b98a98fa..b9119cccb8a6038533511705991012eb757ec989 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -12,6 +12,9 @@
 	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 	<uses-permission android:name="android.permission.VIBRATE" />
+	<!-- FIXME: Only needed for alpha and beta builds -->
+	<uses-permission android:name="android.permission.READ_LOGS" />
+	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
 	<application
 		android:theme="@style/LightTheme"
@@ -54,6 +57,11 @@
 				<category android:name="android.intent.category.LAUNCHER" />
 			</intent-filter>
 		</activity>
+		<activity
+			android:name=".android.TestingActivity"
+			android:logo="@drawable/logo"
+			android:label="@string/app_name" >
+		</activity>
 		<activity
 		    android:name=".android.contact.ContactListActivity"
 			android:logo="@drawable/logo"
diff --git a/briar-android/res/drawable-hdpi/action_help.png b/briar-android/res/drawable-hdpi/action_help.png
new file mode 100644
index 0000000000000000000000000000000000000000..459bed76c5b9f546541383271a6c9b9ff9943093
Binary files /dev/null and b/briar-android/res/drawable-hdpi/action_help.png differ
diff --git a/briar-android/res/drawable-hdpi/social_share.png b/briar-android/res/drawable-hdpi/social_share.png
new file mode 100644
index 0000000000000000000000000000000000000000..47ae186749df92bbbe77f860ce657d3edce27765
Binary files /dev/null and b/briar-android/res/drawable-hdpi/social_share.png differ
diff --git a/briar-android/res/drawable-mdpi/action_help.png b/briar-android/res/drawable-mdpi/action_help.png
new file mode 100644
index 0000000000000000000000000000000000000000..72edd5a761481a3598974dac6d741b31ca953fb7
Binary files /dev/null and b/briar-android/res/drawable-mdpi/action_help.png differ
diff --git a/briar-android/res/drawable-mdpi/social_share.png b/briar-android/res/drawable-mdpi/social_share.png
new file mode 100644
index 0000000000000000000000000000000000000000..8aa52bc7d8a74739dc5e0ad9c35a006bdcbda182
Binary files /dev/null and b/briar-android/res/drawable-mdpi/social_share.png differ
diff --git a/briar-android/res/drawable-xhdpi/action_help.png b/briar-android/res/drawable-xhdpi/action_help.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e67d7c12130fd86e9c7191c236b31aebdfe890e
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/action_help.png differ
diff --git a/briar-android/res/drawable-xhdpi/social_share.png b/briar-android/res/drawable-xhdpi/social_share.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdafd8abca1912ff47f812c38fd29994da1d7867
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/social_share.png differ
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index b25cc1843523f87d764dddef7c035f665adb81a1..8b0ec7923ca707bd4eaa5f1bfca10cce4f4825cf 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -4,6 +4,7 @@
     <color name="action_bar_text">#FFFFFF</color>
     <color name="action_bar_background">#2D3E50</color>
     <color name="button_bar_background">#FFFFFF</color>
+    <color name="dashboard_background">#FFFFFF</color>
     <color name="private_message_background">#FFFFFF</color>
 	<color name="private_message_date">#AAAAAA</color>
     <color name="unread_background">#FFFFFF</color>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 338a01086517004095cfe475453d8f5c57a07837..b452b23ea3e3e03631824fbc47c21abb92948b74 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -16,7 +16,7 @@
     <string name="expiry_warning">This software has expired.\nPlease install a newer version.</string>
     <string name="contact_list_button">Contacts</string>
     <string name="forums_button">Forums</string>
-    <string name="synchronize_button">Synchronize</string>
+    <string name="testing_button">Testing</string>
     <string name="sign_out_button">Sign Out</string>
     <string name="contact_list_title">Contacts</string>
     <string name="no_contacts">No contacts</string>
diff --git a/briar-android/src/org/briarproject/android/DashboardActivity.java b/briar-android/src/org/briarproject/android/DashboardActivity.java
index f1f604f1e764706b2134dd18ab24501580310054..956254c4f1e34ff302b0082af3fece087468d580 100644
--- a/briar-android/src/org/briarproject/android/DashboardActivity.java
+++ b/briar-android/src/org/briarproject/android/DashboardActivity.java
@@ -2,7 +2,6 @@ package org.briarproject.android;
 
 import static android.view.Gravity.CENTER;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.Toast.LENGTH_SHORT;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
@@ -37,7 +36,6 @@ import android.widget.GridView;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.ProgressBar;
-import android.widget.Toast;
 
 public class DashboardActivity extends BriarActivity {
 
@@ -121,20 +119,19 @@ public class DashboardActivity extends BriarActivity {
 		});
 		buttons.add(forumsButton);
 
-		Button syncButton = new Button(this);
-		syncButton.setLayoutParams(matchMatch);
-		syncButton.setBackgroundResource(0);
-		syncButton.setCompoundDrawablesWithIntrinsicBounds(0,
-				R.drawable.navigation_refresh, 0, 0);
-		syncButton.setText(R.string.synchronize_button);
-		syncButton.setOnClickListener(new OnClickListener() {
+		Button testingButton = new Button(this);
+		testingButton.setLayoutParams(matchMatch);
+		testingButton.setBackgroundResource(0);
+		testingButton.setCompoundDrawablesWithIntrinsicBounds(0,
+				R.drawable.action_help, 0, 0);
+		testingButton.setText(R.string.testing_button);
+		testingButton.setOnClickListener(new OnClickListener() {
 			public void onClick(View view) {
-				// FIXME: Hook this button up to an activity
-				Toast.makeText(DashboardActivity.this,
-						R.string.not_implemented_toast, LENGTH_SHORT).show();
+				startActivity(new Intent(DashboardActivity.this,
+						TestingActivity.class));
 			}
 		});
-		buttons.add(syncButton);
+		buttons.add(testingButton);
 
 		Button signOutButton = new Button(this);
 		signOutButton.setLayoutParams(matchMatch);
@@ -157,7 +154,7 @@ public class DashboardActivity extends BriarActivity {
 		grid.setGravity(CENTER);
 		grid.setPadding(pad, pad, pad, pad);
 		Resources res = getResources();
-		grid.setBackgroundColor(res.getColor(R.color.button_bar_background));
+		grid.setBackgroundColor(res.getColor(R.color.dashboard_background));
 		grid.setNumColumns(2);
 		grid.setAdapter(new BaseAdapter() {
 
diff --git a/briar-android/src/org/briarproject/android/TestingActivity.java b/briar-android/src/org/briarproject/android/TestingActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ff9381ce9fad05d4941dab7fe979174cecf6522
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/TestingActivity.java
@@ -0,0 +1,347 @@
+package org.briarproject.android;
+
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+import static android.content.Intent.ACTION_SEND;
+import static android.content.Intent.EXTRA_EMAIL;
+import static android.content.Intent.EXTRA_STREAM;
+import static android.content.Intent.EXTRA_SUBJECT;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
+import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
+import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Scanner;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import org.briarproject.R;
+import org.briarproject.android.util.ElasticHorizontalSpace;
+import org.briarproject.android.util.HorizontalBorder;
+import org.briarproject.android.util.LayoutUtils;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.android.AndroidExecutor;
+import org.briarproject.api.plugins.Plugin;
+import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.util.StringUtils;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+public class TestingActivity extends BriarActivity implements OnClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(TestingActivity.class.getName());
+
+	@Inject private AndroidExecutor androidExecutor;
+	@Inject private PluginManager pluginManager;
+	private ScrollView scroll = null;
+	private LinearLayout status = null;
+	private ImageButton refresh = null, share = null;
+	private File temp = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(state);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		scroll = new ScrollView(this);
+		scroll.setLayoutParams(MATCH_WRAP_1);
+		status = new LinearLayout(this);
+		status.setOrientation(VERTICAL);
+		status.setGravity(CENTER_HORIZONTAL);
+		int pad = LayoutUtils.getPadding(this);
+		status.setPadding(pad, pad, pad, pad);
+		scroll.addView(status);
+		layout.addView(scroll);
+
+		layout.addView(new HorizontalBorder(this));
+
+		LinearLayout footer = new LinearLayout(this);
+		footer.setLayoutParams(MATCH_WRAP);
+		footer.setGravity(CENTER);
+		Resources res = getResources();
+		footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
+		footer.addView(new ElasticHorizontalSpace(this));
+
+		refresh = new ImageButton(this);
+		refresh.setBackgroundResource(0);
+		refresh.setImageResource(R.drawable.navigation_refresh);
+		refresh.setOnClickListener(this);
+		footer.addView(refresh);
+		footer.addView(new ElasticHorizontalSpace(this));
+
+		share = new ImageButton(this);
+		share.setBackgroundResource(0);
+		share.setImageResource(R.drawable.social_share);
+		share.setOnClickListener(this);
+		footer.addView(share);
+		footer.addView(new ElasticHorizontalSpace(this));
+		layout.addView(footer);
+
+		setContentView(layout);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		refresh();
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		if(temp != null) temp.delete();
+	}
+
+	public void onClick(View view) {
+		if(view == refresh) refresh();
+		else if(view == share) share();
+	}
+
+	private void refresh() {
+		status.removeAllViews();
+		new AsyncTask<Void, Void, Map<String, String>>() {
+
+			protected Map<String, String> doInBackground(Void... args) {
+				return getStatusMap();
+			}
+
+			protected void onPostExecute(Map<String, String> result) {
+				int pad = LayoutUtils.getPadding(TestingActivity.this);
+				for(Entry<String, String> e : result.entrySet()) {
+					TextView title = new TextView(TestingActivity.this);
+					title.setTextSize(18);
+					title.setText(e.getKey());
+					status.addView(title);
+					TextView content = new TextView(TestingActivity.this);
+					content.setPadding(0, 0, 0, pad);
+					content.setText(e.getValue());
+					status.addView(content);
+				}
+				scroll.scrollTo(0, 0);
+			}
+		}.execute();
+	}
+
+	private Map<String, String> getStatusMap() {
+		Map<String, String> statusMap = new LinkedHashMap<String, String>();
+		// Is mobile data available?
+		Object o = getSystemService(CONNECTIVITY_SERVICE);
+		ConnectivityManager cm = (ConnectivityManager) o;
+		NetworkInfo mobile = cm.getNetworkInfo(TYPE_MOBILE);
+		boolean mobileAvailable = mobile != null && mobile.isAvailable();
+		// Is mobile data enabled?
+		boolean mobileEnabled = false;
+		try {
+			Class<?> clazz = Class.forName(cm.getClass().getName());
+			Method method = clazz.getDeclaredMethod("getMobileDataEnabled");
+			method.setAccessible(true);
+			mobileEnabled = (Boolean) method.invoke(cm);
+		} catch(ClassNotFoundException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch(NoSuchMethodException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch(IllegalAccessException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch(IllegalArgumentException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch(InvocationTargetException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+		// Is mobile data connected ?
+		boolean mobileConnected = mobile != null && mobile.isConnected();
+
+		// Strings aren't loaded from resources as this activity is temporary
+		String mobileStatus;
+		if(mobileAvailable) mobileStatus = "Available, ";
+		else mobileStatus = "Not available, ";
+		if(mobileEnabled) mobileStatus += "enabled, ";
+		else mobileStatus += "not enabled, ";
+		if(mobileConnected) mobileStatus += "connected";
+		else mobileStatus += "not connected";
+		statusMap.put("Mobile data:", mobileStatus);
+
+		// Is wifi available?
+		NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI);
+		boolean wifiAvailable = wifi != null && wifi.isAvailable();
+		// Is wifi enabled?
+		WifiManager wm = (WifiManager) getSystemService(WIFI_SERVICE);
+		boolean wifiEnabled = wm != null &&
+				wm.getWifiState() == WIFI_STATE_ENABLED;
+		// Is wifi connected?
+		boolean wifiConnected = wifi != null && wifi.isConnected();
+
+		String wifiStatus;
+		if(wifiAvailable) wifiStatus = "Available, ";
+		else wifiStatus = "Not available, ";
+		if(wifiEnabled) wifiStatus += "enabled, ";
+		else wifiStatus += "not enabled, ";
+		if(wifiConnected) wifiStatus += "connected";
+		else wifiStatus += "not connected";
+		statusMap.put("Wi-Fi:", wifiStatus);
+
+		// Is Bluetooth available?
+		BluetoothAdapter bt = null;
+		try {
+			bt = androidExecutor.call(new Callable<BluetoothAdapter>() {
+				public BluetoothAdapter call() throws Exception {
+					return BluetoothAdapter.getDefaultAdapter();
+				}
+			});
+		} catch(InterruptedException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch(ExecutionException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+		boolean btAvailable = bt != null;
+		// Is Bluetooth enabled?
+		boolean btEnabled = bt != null && bt.isEnabled() &&
+				!StringUtils.isNullOrEmpty(bt.getAddress());
+		// Is Bluetooth connectable?
+		boolean btConnectable = bt != null &&
+				(bt.getScanMode() == SCAN_MODE_CONNECTABLE ||
+				bt.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+		// Is Bluetooth discoverable?
+		boolean btDiscoverable = bt != null &&
+				bt.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+
+		String btStatus;
+		if(btAvailable) btStatus = "Available, ";
+		else btStatus = "Not available, ";
+		if(btEnabled) btStatus += "enabled, ";
+		else btStatus += "not enabled, ";
+		if(btConnectable) btStatus += "connectable, ";
+		else btStatus += "not connectable, ";
+		if(btDiscoverable) btStatus += "discoverable";
+		else btStatus += "not discoverable";
+		statusMap.put("Bluetooth:", btStatus);
+
+		Plugin torPlugin = pluginManager.getPlugin(new TransportId("tor"));
+		boolean torPluginEnabled = torPlugin != null;
+		boolean torPluginRunning = torPlugin != null && torPlugin.isRunning();
+
+		String torPluginStatus;
+		if(torPluginEnabled) torPluginStatus = "Enabled, ";
+		else torPluginStatus = "Not enabled, ";
+		if(torPluginRunning) torPluginStatus += "running";
+		else torPluginStatus += "not running";
+		statusMap.put("Tor plugin:", torPluginStatus);
+
+		Plugin lanPlugin = pluginManager.getPlugin(new TransportId("lan"));
+		boolean lanPluginEnabled = lanPlugin != null;
+		boolean lanPluginRunning = lanPlugin != null && lanPlugin.isRunning();
+
+		String lanPluginStatus;
+		if(lanPluginEnabled) lanPluginStatus = "Enabled, ";
+		else lanPluginStatus = "Not enabled, ";
+		if(lanPluginRunning) lanPluginStatus += "running";
+		else lanPluginStatus += "not running";
+		statusMap.put("LAN plugin:", lanPluginStatus);
+
+		Plugin btPlugin = pluginManager.getPlugin(new TransportId("bt"));
+		boolean btPluginEnabled = btPlugin != null;
+		boolean btPluginRunning = btPlugin != null && btPlugin.isRunning();
+
+		String btPluginStatus;
+		if(btPluginEnabled) btPluginStatus = "Enabled, ";
+		else btPluginStatus = "Not enabled, ";
+		if(btPluginRunning) btPluginStatus += "running";
+		else btPluginStatus += "not running";
+		statusMap.put("Bluetooth plugin:", btPluginStatus);
+
+		StringBuilder log = new StringBuilder();
+		try {
+			Runtime runtime = Runtime.getRuntime();
+			Process process = runtime.exec("logcat -d -s TorPlugin");
+			Scanner scanner = new Scanner(process.getInputStream());
+			while(scanner.hasNextLine()) {
+				log.append(scanner.nextLine());
+				log.append('\n');
+			}
+			scanner.close();
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+		statusMap.put("Tor log:", log.toString());
+
+		return Collections.unmodifiableMap(statusMap);
+	}
+
+	private void share() {
+		new AsyncTask<Void, Void, Map<String, String>>() {
+
+			protected Map<String, String> doInBackground(Void... args) {
+				return getStatusMap();
+			}
+
+			protected void onPostExecute(Map<String, String> result) {
+				try {
+					File shared = Environment.getExternalStorageDirectory();
+					temp = File.createTempFile("debug", "txt", shared);
+					if(LOG.isLoggable(INFO))
+						LOG.info("Writing to " + temp.getPath());
+					PrintStream p = new PrintStream(new FileOutputStream(temp));
+					for(Entry<String, String> e : result.entrySet()) {
+						p.println(e.getKey());
+						p.println(e.getValue());
+						p.println();
+					}
+					p.flush();
+					p.close();
+					sendEmail(Uri.fromFile(temp));
+				} catch(IOException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		}.execute();
+	}
+
+	private void sendEmail(Uri attachment) {
+		Intent i = new Intent(ACTION_SEND);
+		i.setType("message/rfc822");
+		i.putExtra(EXTRA_EMAIL, new String[] { "debug@briarproject.org" });
+		i.putExtra(EXTRA_SUBJECT, "Debugging information");
+		i.putExtra(EXTRA_STREAM, attachment);
+		startActivity(Intent.createChooser(i, "Send to developers"));
+	}
+}
diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
index b2609f435d8718ceff9771235a6f972ad756a42c..4a875f5fb661eddfd8f34e15393adefcd3ef9a9e 100644
--- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
@@ -68,6 +68,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 
 	private volatile boolean running = false;
 	private volatile boolean wasEnabled = false, isEnabled = false;
+	private volatile BluetoothServerSocket socket = null;
 
 	// Non-null if running has ever been true
 	private volatile BluetoothAdapter adapter = null;
@@ -149,7 +150,8 @@ class DroidtoothPlugin implements DuplexPlugin {
 			tryToClose(ss);
 			return;
 		}
-		acceptContactConnections(ss);
+		socket = ss;
+		acceptContactConnections();
 	}
 
 	private boolean enableBluetooth() {
@@ -198,15 +200,15 @@ class DroidtoothPlugin implements DuplexPlugin {
 		}
 	}
 
-	private void acceptContactConnections(BluetoothServerSocket ss) {
+	private void acceptContactConnections() {
 		while(true) {
 			BluetoothSocket s;
 			try {
-				s = ss.accept();
+				s = socket.accept();
 			} catch(IOException e) {
 				// This is expected when the socket is closed
 				if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
-				tryToClose(ss);
+				tryToClose(socket);
 				return;
 			}
 			callback.incomingConnectionCreated(wrapSocket(s));
@@ -220,6 +222,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 
 	public void stop() {
 		running = false;
+		if(socket != null) tryToClose(socket);
 		// Disable Bluetooth if we enabled it at startup
 		if(isEnabled && !wasEnabled) disableBluetooth();
 	}
@@ -246,6 +249,10 @@ class DroidtoothPlugin implements DuplexPlugin {
 		}
 	}
 
+	public boolean isRunning() {
+		return running && socket != null;
+	}
+
 	public boolean shouldPoll() {
 		return true;
 	}
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
index ed526aee2db483a684c6c262824043402f6a009c..80ac29e959d5a1c7f1e892eb8d7b9bc31789fcda 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
@@ -72,7 +72,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 	private final File torDirectory, torFile, geoIpFile, configFile, doneFile;
 	private final File cookieFile, pidFile, hostnameFile;
 
-	private volatile boolean running = false;
+	private volatile boolean running = false, networkEnabled = false;
 	private volatile Process tor = null;
 	private volatile int pid = -1;
 	private volatile ServerSocket socket = null;
@@ -509,6 +509,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 		if(!running) return;
 		if(LOG.isLoggable(INFO)) LOG.info("Enabling network: " + enable);
 		controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
+		networkEnabled = enable;
 	}
 
 	public void stop() throws IOException {
@@ -534,6 +535,10 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 		}
 	}
 
+	public boolean isRunning() {
+		return running && networkEnabled;
+	}
+
 	public boolean shouldPoll() {
 		return true;
 	}
diff --git a/briar-api/src/org/briarproject/api/plugins/Plugin.java b/briar-api/src/org/briarproject/api/plugins/Plugin.java
index 0eb66620ee8ff72d75bbd48b5bf67a0889d17995..3389a73577d9da1ce789a25d3387d2ae87fe9b16 100644
--- a/briar-api/src/org/briarproject/api/plugins/Plugin.java
+++ b/briar-api/src/org/briarproject/api/plugins/Plugin.java
@@ -23,6 +23,9 @@ public interface Plugin {
 	/** Stops the plugin. */
 	void stop() throws IOException;
 
+	/** Returns true if the plugin is running. */
+	boolean isRunning();
+
 	/**
 	 * Returns true if the plugin's {@link #poll(Collection)} method should be
 	 * called periodically to attempt to establish connections.
diff --git a/briar-api/src/org/briarproject/api/plugins/PluginManager.java b/briar-api/src/org/briarproject/api/plugins/PluginManager.java
index 330de8669bab1285125bd59c169b843ca2c31bd6..efc336cf020c17686bd1441e50202828b437fecc 100644
--- a/briar-api/src/org/briarproject/api/plugins/PluginManager.java
+++ b/briar-api/src/org/briarproject/api/plugins/PluginManager.java
@@ -2,6 +2,7 @@ package org.briarproject.api.plugins;
 
 import java.util.Collection;
 
+import org.briarproject.api.TransportId;
 import org.briarproject.api.lifecycle.Service;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 
@@ -11,6 +12,12 @@ import org.briarproject.api.plugins.duplex.DuplexPlugin;
  */
 public interface PluginManager extends Service {
 
+	/**
+	 * Returns the plugin for the given transport, or null if no such plugin
+	 * is running.
+	 */
+	Plugin getPlugin(TransportId t);
+
 	/** Returns any running duplex plugins that support invitations. */
 	Collection<DuplexPlugin> getInvitationPlugins();
 }
diff --git a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
index b78aba1bb29c8f56633c954892651bb6c5ae8e2a..6dee08bb2c905de613e2599e0a730ff49269d686 100644
--- a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
@@ -9,6 +9,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -56,6 +57,7 @@ class PluginManagerImpl implements PluginManager {
 	private final Poller poller;
 	private final ConnectionDispatcher dispatcher;
 	private final UiCallback uiCallback;
+	private final Map<TransportId, Plugin> plugins;
 	private final List<SimplexPlugin> simplexPlugins;
 	private final List<DuplexPlugin> duplexPlugins;
 
@@ -73,6 +75,7 @@ class PluginManagerImpl implements PluginManager {
 		this.poller = poller;
 		this.dispatcher = dispatcher;
 		this.uiCallback = uiCallback;
+		plugins = new ConcurrentHashMap<TransportId, Plugin>();
 		simplexPlugins = new CopyOnWriteArrayList<SimplexPlugin>();
 		duplexPlugins = new CopyOnWriteArrayList<DuplexPlugin>();
 	}
@@ -104,10 +107,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);
-		poller.start(Collections.unmodifiableList(plugins));
+		poller.start(plugins.values());
 		return true;
 	}
 
@@ -115,8 +115,7 @@ class PluginManagerImpl implements PluginManager {
 		// Stop the poller
 		if(LOG.isLoggable(INFO)) LOG.info("Stopping poller");
 		poller.stop();
-		int plugins = simplexPlugins.size() + duplexPlugins.size();
-		final CountDownLatch latch = new CountDownLatch(plugins);
+		final CountDownLatch latch = new CountDownLatch(plugins.size());
 		// Stop the simplex plugins
 		if(LOG.isLoggable(INFO)) LOG.info("Stopping simplex plugins");
 		for(SimplexPlugin plugin : simplexPlugins)
@@ -125,6 +124,7 @@ class PluginManagerImpl implements PluginManager {
 		if(LOG.isLoggable(INFO)) LOG.info("Stopping duplex plugins");
 		for(DuplexPlugin plugin : duplexPlugins)
 			pluginExecutor.execute(new PluginStopper(plugin, latch));
+		plugins.clear();
 		simplexPlugins.clear();
 		duplexPlugins.clear();
 		// Wait for all the plugins to stop
@@ -139,6 +139,10 @@ class PluginManagerImpl implements PluginManager {
 		return true;
 	}
 
+	public Plugin getPlugin(TransportId t) {
+		return plugins.get(t);
+	}
+
 	public Collection<DuplexPlugin> getInvitationPlugins() {
 		List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
 		for(DuplexPlugin d : duplexPlugins)
@@ -185,6 +189,7 @@ class PluginManagerImpl implements PluginManager {
 					boolean started = plugin.start();
 					long duration = clock.currentTimeMillis() - start;
 					if(started) {
+						plugins.put(id, plugin);
 						simplexPlugins.add(plugin);
 						if(LOG.isLoggable(INFO)) {
 							String name = plugin.getClass().getSimpleName();
@@ -246,6 +251,7 @@ class PluginManagerImpl implements PluginManager {
 					boolean started = plugin.start();
 					long duration = clock.currentTimeMillis() - start;
 					if(started) {
+						plugins.put(id, plugin);
 						duplexPlugins.add(plugin);
 						if(LOG.isLoggable(INFO)) {
 							String name = plugin.getClass().getSimpleName();
diff --git a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
index 6b286559aad9fcbec39a17f27f661419093aebec..77ed42e42c0d4b5d5a2a8b2eded20be8e2abb50b 100644
--- a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
+++ b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
@@ -55,6 +55,10 @@ public abstract class FilePlugin implements SimplexPlugin {
 		return maxLatency;
 	}
 
+	public boolean isRunning() {
+		return running;
+	}
+
 	public SimplexTransportReader createReader(ContactId c) {
 		return null;
 	}
diff --git a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
index 4f483dc89e808c69a72e416d8f519a4bc2844828..13401c290c7a3f873e41cc3ff618de7ec8a32521 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
@@ -146,6 +146,10 @@ abstract class TcpPlugin implements DuplexPlugin {
 		if(socket != null) tryToClose(socket);
 	}
 
+	public boolean isRunning() {
+		return running && socket != null && socket.isBound();
+	}
+
 	public boolean shouldPoll() {
 		return true;
 	}
diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
index 3bc65ce0f785aed83137c7457e6838caac57b037..4572df83e13c2a3213af9f8da414f6553bb6b3a6 100644
--- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
@@ -172,6 +172,10 @@ class BluetoothPlugin implements DuplexPlugin {
 		tryToClose(socket);
 	}
 
+	public boolean isRunning() {
+		return running;
+	}
+
 	public boolean shouldPoll() {
 		return true;
 	}
diff --git a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
index 58f1c421e363c0d1f19a6856a330799aae100e13..4913cd82dbc0535b64be69e9f45d8a28874f2464 100644
--- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
@@ -98,23 +98,8 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 		}
 	}
 
-	private boolean resetModem() {
-		if(!running) return false;
-		for(String portName : serialPortList.getPortNames()) {
-			if(LOG.isLoggable(INFO))
-				LOG.info("Trying to initialise modem on " + portName);
-			modem = modemFactory.createModem(this, portName);
-			try {
-				if(!modem.start()) continue;
-				if(LOG.isLoggable(INFO))
-					LOG.info("Initialised modem on " + portName);
-				return true;
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-		running = false;
-		return false;
+	public boolean isRunning() {
+		return running;
 	}
 
 	public boolean shouldPoll() {
@@ -180,6 +165,25 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 		}
 	}
 
+	private boolean resetModem() {
+		if(!running) return false;
+		for(String portName : serialPortList.getPortNames()) {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Trying to initialise modem on " + portName);
+			modem = modemFactory.createModem(this, portName);
+			try {
+				if(!modem.start()) continue;
+				if(LOG.isLoggable(INFO))
+					LOG.info("Initialised modem on " + portName);
+				return true;
+			} catch(IOException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			}
+		}
+		running = false;
+		return false;
+	}
+
 	public DuplexTransportConnection createConnection(ContactId c) {
 		if(!running) return null;
 		// Get the ISO 3166 code for the caller's country