diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 7e3632ac7e285bccfbca167394ed0d4d8dcb7187..6cd2adce34ae4f3305c13c70c106aa490f7fee22 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -17,6 +17,7 @@
 	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
 	<application
+		android:name=".android.BriarApplication"
 		android:theme="@style/LightTheme"
 		android:icon="@drawable/ic_launcher"
 		android:label="@string/app_name"
@@ -28,6 +29,15 @@
 				<action android:name="org.briarproject.android.BriarService" />
 			</intent-filter>
 		</service>
+		<activity
+			android:name=".android.CrashReportActivity"
+			android:logo="@drawable/logo"
+			android:label="@string/crash_report_title" >
+			<intent-filter>
+				<action android:name="org.briarproject.REPORT_CRASH" />
+				<category android:name="android.intent.category.DEFAULT" />
+			</intent-filter>
+		</activity>
 		<activity
 			android:name=".android.DashboardActivity"
 			android:logo="@drawable/logo"
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 7bed85b95ea4418da095dd6363b8013f2158aa05..47d1a9fb585f7706bc31eaf7d516961bac2feb6a 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="app_name">Briar</string>
+    <string name="crash_report_title">Briar Crash Report</string>
     <string name="ongoing_notification_title">Signed into Briar</string>
     <string name="ongoing_notification_text">Touch to show the dashboard.</string>
     <string name="setup_title">Briar Setup</string>
diff --git a/briar-android/src/org/briarproject/android/BriarApplication.java b/briar-android/src/org/briarproject/android/BriarApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e54544cfe8e248db67e28692f42894d0f02fd94
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/BriarApplication.java
@@ -0,0 +1,24 @@
+package org.briarproject.android;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.logging.Logger;
+
+import android.app.Application;
+import android.content.Context;
+
+public class BriarApplication extends Application {
+
+	private static final Logger LOG =
+			Logger.getLogger(BriarApplication.class.getName());
+
+	@Override
+	public void onCreate() {
+		super.onCreate();
+		LOG.info("Created");
+		UncaughtExceptionHandler oldHandler =
+				Thread.getDefaultUncaughtExceptionHandler();
+		Context ctx = getApplicationContext();
+		CrashHandler newHandler = new CrashHandler(ctx, oldHandler);
+		Thread.setDefaultUncaughtExceptionHandler(newHandler);
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/CrashHandler.java b/briar-android/src/org/briarproject/android/CrashHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..757f999b27517c1aa3a34b5c16a01499c6a745f6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/CrashHandler.java
@@ -0,0 +1,44 @@
+package org.briarproject.android;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static java.util.logging.Level.WARNING;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.logging.Logger;
+
+import android.content.Context;
+import android.content.Intent;
+
+class CrashHandler implements UncaughtExceptionHandler {
+
+	private static final Logger LOG =
+			Logger.getLogger(CrashHandler.class.getName());
+
+	private final Context ctx;
+	private final UncaughtExceptionHandler delegate; // May be null
+
+	CrashHandler(Context ctx, UncaughtExceptionHandler delegate) {
+		this.ctx = ctx;
+		this.delegate = delegate;
+	}
+
+	public void uncaughtException(Thread thread, Throwable throwable) {
+		LOG.log(WARNING, "Uncaught exception", throwable);
+		// Get the stack trace
+		StringWriter sw = new StringWriter();
+		PrintWriter pw = new PrintWriter(sw);
+		throwable.printStackTrace(pw);
+		String stackTrace = sw.toString();
+		// Launch the crash reporting dialog
+		Intent i = new Intent();
+		i.setAction("org.briarproject.REPORT_CRASH");
+		i.setFlags(FLAG_ACTIVITY_NEW_TASK);
+		i.putExtra("briar.STACK_TRACE", stackTrace);
+		i.putExtra("briar.PID", android.os.Process.myPid());
+		ctx.startActivity(i);
+		// Pass the exception to the default handler, if any
+		if(delegate != null) delegate.uncaughtException(thread, throwable);
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/CrashReportActivity.java b/briar-android/src/org/briarproject/android/CrashReportActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d8520a874a96be7ad5277427700380980d25ec6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/CrashReportActivity.java
@@ -0,0 +1,430 @@
+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.view.View.GONE;
+import static android.view.View.VISIBLE;
+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.TestingConstants.SHARE_CRASH_REPORTS;
+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 java.util.regex.Pattern;
+
+import org.briarproject.R;
+import org.briarproject.android.util.HorizontalBorder;
+import org.briarproject.android.util.LayoutUtils;
+import org.briarproject.android.util.ListLoadingProgressBar;
+import org.briarproject.api.android.AndroidExecutor;
+import org.briarproject.api.system.FileUtils;
+import org.briarproject.system.AndroidFileUtils;
+import org.briarproject.util.StringUtils;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+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.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.Build;
+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 CrashReportActivity extends Activity implements OnClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(CrashReportActivity.class.getName());
+
+	private final FileUtils fileUtils = new AndroidFileUtils();
+	private final AndroidExecutor androidExecutor = new AndroidExecutorImpl();
+
+	private ScrollView scroll = null;
+	private ListLoadingProgressBar progress = null;
+	private LinearLayout status = null;
+	private ImageButton share = null;
+	private File temp = null;
+
+	private volatile String stack = null;
+	private volatile int pid = -1;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(state);
+
+		Intent i = getIntent();
+		stack = i.getStringExtra("briar.STACK_TRACE");
+		pid = i.getIntExtra("briar.PID", -1);
+
+		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);
+
+		progress = new ListLoadingProgressBar(this);
+		progress.setVisibility(GONE);
+		layout.addView(progress);
+
+		if(SHARE_CRASH_REPORTS) {
+			layout.addView(new HorizontalBorder(this));
+			LinearLayout footer = new LinearLayout(this);
+			footer.setLayoutParams(MATCH_WRAP);
+			footer.setGravity(CENTER);
+			Resources res = getResources();
+			int background = res.getColor(R.color.button_bar_background);
+			footer.setBackgroundColor(background);
+			share = new ImageButton(this);
+			share.setBackgroundResource(0);
+			share.setImageResource(R.drawable.social_share);
+			share.setOnClickListener(this);
+			footer.addView(share);
+			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) {
+		share();
+	}
+
+	private void refresh() {
+		status.removeAllViews();
+		scroll.setVisibility(GONE);
+		progress.setVisibility(VISIBLE);
+		new AsyncTask<Void, Void, Map<String, String>>() {
+
+			protected Map<String, String> doInBackground(Void... args) {
+				return getStatusMap();
+			}
+
+			protected void onPostExecute(Map<String, String> result) {
+				Context ctx = CrashReportActivity.this;
+				int pad = LayoutUtils.getPadding(ctx);
+				for(Entry<String, String> e : result.entrySet()) {
+					TextView title = new TextView(ctx);
+					title.setTextSize(18);
+					title.setText(e.getKey());
+					status.addView(title);
+					TextView content = new TextView(ctx);
+					content.setPadding(0, 0, 0, pad);
+					content.setText(e.getValue());
+					status.addView(content);
+				}
+				scroll.setVisibility(VISIBLE);
+				progress.setVisibility(GONE);
+			}
+		}.execute();
+	}
+
+	// FIXME: Load strings from resources if we're keeping this activity
+	@SuppressLint("NewApi")
+	private Map<String, String> getStatusMap() {
+		Map<String, String> statusMap = new LinkedHashMap<String, String>();
+
+		// Device type
+		String deviceType;
+		String manufacturer = Build.MANUFACTURER;
+		String model = Build.MODEL;
+		String brand = Build.BRAND;
+		if(model.startsWith(manufacturer)) deviceType = capitalize(model);
+		else deviceType = capitalize(manufacturer) + " " + model;
+		if(!StringUtils.isNullOrEmpty(brand))
+			deviceType += " (" + capitalize(brand) + ")";
+		statusMap.put("Device type:", deviceType);
+
+		// Android version
+		String release = Build.VERSION.RELEASE;
+		int sdk = Build.VERSION.SDK_INT;
+		statusMap.put("Android version:", release + " (" + sdk + ")");
+
+		// CPU architecture
+		statusMap.put("Architecture:", Build.CPU_ABI);
+
+		// System memory
+		Object o = getSystemService(ACTIVITY_SERVICE);
+		ActivityManager am = (ActivityManager) o;
+		ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
+		am.getMemoryInfo(mem);
+		String systemMemory;
+		if(Build.VERSION.SDK_INT >= 16) {
+			systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
+					+ (mem.availMem / 1024 / 1204) + " MiB free, "
+					+ (mem.threshold / 1024 / 1024) + " MiB threshold";
+		} else {
+			systemMemory = (mem.availMem / 1024 / 1204) + " MiB free, "
+					+ (mem.threshold / 1024 / 1024) + " MiB threshold";
+		}
+		statusMap.put("System memory:", systemMemory);
+
+		// Virtual machine memory
+		Runtime runtime = Runtime.getRuntime();
+		long heap = runtime.totalMemory();
+		long heapFree = runtime.freeMemory();
+		long heapMax = runtime.maxMemory();
+		String vmMemory = (heap / 1024 / 1024) + " MiB allocated, "
+				+ (heapFree / 1024 / 1024) + " MiB free, "
+				+ (heapMax / 1024 / 1024) + " MiB maximum";
+		statusMap.put("Virtual machine memory:", vmMemory);
+
+		// Internal storage
+		try {
+			File root = Environment.getRootDirectory();
+			long rootTotal = fileUtils.getTotalSpace(root);
+			long rootFree = fileUtils.getFreeSpace(root);
+			String internal = (rootTotal / 1024 / 1024) + " MiB total, "
+					+ (rootFree / 1024 / 1024) + " MiB free";
+			statusMap.put("Internal storage:", internal);
+		} catch(IOException e) {
+			statusMap.put("Internal storage:", "Unknown");
+		}
+
+		// External storage (SD card)
+		try {
+			File sd = Environment.getExternalStorageDirectory();
+			long sdTotal = fileUtils.getTotalSpace(sd);
+			long sdFree = fileUtils.getFreeSpace(sd);
+			String external = (sdTotal / 1024 / 1024) + " MiB total, "
+					+ (sdFree / 1024 / 1024) + " MiB free";
+			statusMap.put("External storage:", external);
+		} catch(IOException e) {
+			statusMap.put("External storage:", "Unknown");
+		}
+
+		// Is mobile data available?
+		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();
+
+		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";
+		if(wm != null) {
+			WifiInfo wifiInfo = wm.getConnectionInfo();
+			if(wifiInfo != null) {
+				int ip = wifiInfo.getIpAddress(); // Nice API, Google
+				int ip1 = ip & 0xFF;
+				int ip2 = (ip >> 8) & 0xFF;
+				int ip3 = (ip >> 16) & 0xFF;
+				int ip4 = (ip >> 24) & 0xFF;
+				String address = ip1 + "." + ip2 + "." + ip3 + "." + ip4;
+				wifiStatus += "\nAddress: " + address;
+			}
+		}
+		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) {
+			LOG.warning("Interrupted while getting BluetoothAdapter");
+			Thread.currentThread().interrupt();
+		} 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";
+		if(bt != null) btStatus += "\nAddress: " + bt.getAddress();
+		statusMap.put("Bluetooth:", btStatus);
+
+		// Stack trace
+		if(stack != null) statusMap.put("Stack trace:", stack);
+
+		// All log output from the crashed process
+		if(pid != -1) {
+			StringBuilder log = new StringBuilder();
+			try {
+				Pattern pattern = Pattern.compile(".*\\( *" + pid + "\\).*");
+				Process process = runtime.exec("logcat -d -v time *:I");
+				Scanner scanner = new Scanner(process.getInputStream());
+				while(scanner.hasNextLine()) {
+					String line = scanner.nextLine();
+					if(pattern.matcher(line).matches()) {
+						log.append(line);
+						log.append('\n');
+					}
+				}
+				scanner.close();
+			} catch(IOException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			}
+			statusMap.put("Debugging log:", log.toString());
+		}
+
+		return Collections.unmodifiableMap(statusMap);
+	}
+
+	private String capitalize(String s) {
+		if(StringUtils.isNullOrEmpty(s)) return s;
+		char first = s.charAt(0);
+		if(Character.isUpperCase(first)) return s;
+		return Character.toUpperCase(first) + s.substring(1);
+	}
+
+	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("crash", ".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[] { "briartest@gmail.com" });
+		i.putExtra(EXTRA_SUBJECT, "Crash report");
+		i.putExtra(EXTRA_STREAM, attachment);
+		startActivity(Intent.createChooser(i, "Send to developers"));
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/TestingConstants.java b/briar-android/src/org/briarproject/android/TestingConstants.java
index 4d053d32c9b2cd48f8a5ef75c481c8b51435e9b5..e4ad6f8648f3fb3c2cb00f59f220e3b71b05e146 100644
--- a/briar-android/src/org/briarproject/android/TestingConstants.java
+++ b/briar-android/src/org/briarproject/android/TestingConstants.java
@@ -22,4 +22,10 @@ interface TestingConstants {
 	 * This should be false for release builds.
 	 */
 	boolean SHOW_TESTING_ACTIVITY = true;
+
+	/**
+	 * Whether to allow crash reports to be submitted by email. This should
+	 * be false for release builds.
+	 */
+	boolean SHARE_CRASH_REPORTS = true;
 }
diff --git a/briar-android/src/org/briarproject/system/AndroidFileUtils.java b/briar-android/src/org/briarproject/system/AndroidFileUtils.java
index 1574c501a27dd8d25da1e97d6eac91ab0cbb0d2a..82916876676d451d62216225415a6ebd49b4445f 100644
--- a/briar-android/src/org/briarproject/system/AndroidFileUtils.java
+++ b/briar-android/src/org/briarproject/system/AndroidFileUtils.java
@@ -7,7 +7,7 @@ import android.annotation.SuppressLint;
 import android.os.Build;
 import android.os.StatFs;
 
-class AndroidFileUtils implements FileUtils {
+public class AndroidFileUtils implements FileUtils {
 
 	@SuppressLint("NewApi")
 	@SuppressWarnings("deprecation")