From f73f0aa4ab28353941887ef33137fdbd09d4eb11 Mon Sep 17 00:00:00 2001
From: str4d <str4d@mail.i2p>
Date: Sat, 2 Apr 2016 05:05:33 +0000
Subject: [PATCH] Migrate crash reports to ACRA

---
 briar-android/AndroidManifest.xml             |  10 +-
 briar-android/build.gradle                    |   5 +
 .../res/drawable/ic_warning_black_24dp.xml    |   9 +
 briar-android/res/layout/activity_crash.xml   | 125 ++++--
 briar-android/res/values/strings.xml          |  11 +-
 briar-android/res/xml/settings.xml            |  29 ++
 .../android/AndroidComponent.java             |   3 +
 .../android/BriarApplication.java             |  28 +-
 .../briarproject/android/CrashHandler.java    |  46 --
 .../android/CrashReportActivity.java          | 401 ++++++------------
 .../android/util/AndroidUtils.java            |   7 +-
 .../android/util/BriarReportPrimer.java       | 184 ++++++++
 .../android/util/BriarReportSender.java       |  50 +++
 .../util/BriarReportSenderFactory.java        |  20 +
 .../briarproject/plugins/tor/TorPlugin.java   |   2 +-
 .../api/reporting/DevReporter.java            |   4 +-
 .../reporting/DevReporterImpl.java            |   5 +-
 17 files changed, 549 insertions(+), 390 deletions(-)
 create mode 100644 briar-android/res/drawable/ic_warning_black_24dp.xml
 delete mode 100644 briar-android/src/org/briarproject/android/CrashHandler.java
 create mode 100644 briar-android/src/org/briarproject/android/util/BriarReportPrimer.java
 create mode 100644 briar-android/src/org/briarproject/android/util/BriarReportSender.java
 create mode 100644 briar-android/src/org/briarproject/android/util/BriarReportSenderFactory.java

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index dc0874e3b0..51811047ba 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -2,11 +2,11 @@
 <manifest
 	package="org.briarproject"
 	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:versionCode="11"
 	android:versionName="0.11">
 
 	<uses-sdk
-		xmlns:tools="http://schemas.android.com/tools"
 		android:minSdkVersion="9"
 
 		android:targetSdkVersion="22"
@@ -45,14 +45,10 @@
 			android:name=".android.CrashReportActivity"
 			android:excludeFromRecents="true"
 			android:exported="false"
+			android:finishOnTaskLaunch="true"
 			android:label="@string/crash_report_title"
 			android:launchMode="singleInstance"
-			android:process=":briar_error_handler"
-			android:taskAffinity="org.briarproject.android.CrashHandler">
-			<intent-filter>
-				<action android:name="org.briarproject.REPORT_CRASH"/>
-				<category android:name="android.intent.category.DEFAULT"/>
-			</intent-filter>
+			android:process=":briar_error_handler">
 		</activity>
 		<activity
 			android:name=".android.ExpiredActivity"
diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index 87ea40e8d5..3c4204ba65 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -35,6 +35,10 @@ dependencies {
 		exclude module: 'support-v4'
 		exclude module: 'recyclerview-v7'
 	}
+	compile ("ch.acra:acra:4.8.5") {
+		exclude module: 'support-v4'
+		exclude module: 'support-annotations'
+	}
 	compile "info.guardianproject.panic:panic:0.5"
 	compile "info.guardianproject.trustedintents:trustedintents:0.2"
 	compile "de.hdodenhof:circleimageview:2.0.0"
@@ -52,6 +56,7 @@ dependencyVerification {
 			'com.android.support:design:41a9cd75ca78f25df5f573db7cedf8bb66beae00c330943923ba9f3e2051736d',
 			'com.android.support:support-annotations:f347a35b9748a4103b39a6714a77e2100f488d623fd6268e259c177b200e9d82',
 			'com.android.support:recyclerview-v7:7606373da0931a1e62588335465a0e390cd676c98117edab29220317495faefd',
+			'ch.acra:acra:afd5b28934d5166b55f261c85685ad59e8a4ebe9ca1960906afaa8c76d8dc9eb',
 			'info.guardianproject.panic:panic:a7ed9439826db2e9901649892cf9afbe76f00991b768d8f4c26332d7c9406cb2',
 			'info.guardianproject.trustedintents:trustedintents:6221456d8821a8d974c2acf86306900237cf6afaaa94a4c9c44e161350f80f3e',
 			'de.hdodenhof:circleimageview:c76d936395b50705a3f98c9220c22d2599aeb9e609f559f6048975cfc1f686b8',
diff --git a/briar-android/res/drawable/ic_warning_black_24dp.xml b/briar-android/res/drawable/ic_warning_black_24dp.xml
new file mode 100644
index 0000000000..b3a9e036b4
--- /dev/null
+++ b/briar-android/res/drawable/ic_warning_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
+</vector>
diff --git a/briar-android/res/layout/activity_crash.xml b/briar-android/res/layout/activity_crash.xml
index e412ec3745..bf68dc87a2 100644
--- a/briar-android/res/layout/activity_crash.xml
+++ b/briar-android/res/layout/activity_crash.xml
@@ -1,47 +1,98 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout
+<LinearLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
-	android:layout_height="match_parent">
+	android:layout_height="match_parent"
+	android:orientation="vertical">
 
-	<ScrollView
+	<android.support.v7.widget.Toolbar
+		style="@style/BriarToolbar"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content">
 
+		<TextView
+			style="@style/TextAppearance.AppCompat.Large.Inverse"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:text="@string/crash_report_title"/>
+	</android.support.v7.widget.Toolbar>
+
+	<RelativeLayout
+		android:layout_width="match_parent"
+		android:layout_height="match_parent">
+
 		<LinearLayout
-			android:id="@+id/crash_status"
-			android:layout_width="wrap_content"
+			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
-			android:gravity="center_horizontal"
 			android:orientation="vertical"
-			android:paddingBottom="@dimen/listitem_height_one_line_avatar"
-			android:paddingEnd="@dimen/margin_large"
-			android:paddingLeft="@dimen/margin_large"
-			android:paddingRight="@dimen/margin_large"
-			android:paddingStart="@dimen/margin_large"
-			android:paddingTop="@dimen/margin_large"/>
-	</ScrollView>
-
-	<ProgressBar
-		android:id="@+id/progress_wheel"
-		style="?android:attr/progressBarStyleLarge"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_centerInParent="true"
-		android:indeterminate="true"/>
-
-	<android.support.design.widget.FloatingActionButton
-		android:id="@+id/share_crash_report"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_alignParentBottom="true"
-		android:layout_alignParentEnd="true"
-		android:layout_alignParentRight="true"
-		android:layout_marginBottom="@dimen/margin_large"
-		android:layout_marginEnd="@dimen/margin_large"
-		android:layout_marginRight="@dimen/margin_large"
-		android:background="@color/briar_accent"
-		android:src="@drawable/social_share"
-		android:tint="@color/action_bar_text"/>
-
-</RelativeLayout>
\ No newline at end of file
+			android:paddingTop="@dimen/margin_medium">
+
+			<EditText
+				android:id="@+id/user_comment"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:layout_marginEnd="@dimen/margin_large"
+				android:layout_marginLeft="@dimen/margin_large"
+				android:layout_marginRight="@dimen/margin_large"
+				android:layout_marginStart="@dimen/margin_large"
+				android:hint="@string/describe_crash"
+				android:inputType="textMultiLine|textCapSentences"/>
+
+			<EditText
+				android:id="@+id/user_email"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:layout_marginEnd="@dimen/margin_large"
+				android:layout_marginLeft="@dimen/margin_large"
+				android:layout_marginRight="@dimen/margin_large"
+				android:layout_marginStart="@dimen/margin_large"
+				android:layout_marginTop="@dimen/margin_small"
+				android:hint="@string/optional_contact_email"
+				android:inputType="textEmailAddress"
+				android:maxLines="1"/>
+
+			<ScrollView
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:layout_marginTop="@dimen/margin_small">
+
+				<LinearLayout
+					android:id="@+id/crash_status"
+					android:layout_width="match_parent"
+					android:layout_height="wrap_content"
+					android:gravity="center_horizontal"
+					android:orientation="vertical"
+					android:paddingBottom="@dimen/listitem_height_one_line_avatar"
+					android:paddingEnd="@dimen/margin_large"
+					android:paddingLeft="@dimen/margin_large"
+					android:paddingRight="@dimen/margin_large"
+					android:paddingStart="@dimen/margin_large"
+					android:paddingTop="@dimen/margin_small"/>
+
+			</ScrollView>
+		</LinearLayout>
+
+		<ProgressBar
+			android:id="@+id/progress_wheel"
+			style="?android:attr/progressBarStyleLarge"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_centerInParent="true"
+			android:indeterminate="true"/>
+
+		<android.support.design.widget.FloatingActionButton
+			android:id="@+id/share_crash_report"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentBottom="true"
+			android:layout_alignParentEnd="true"
+			android:layout_alignParentRight="true"
+			android:layout_marginBottom="@dimen/margin_large"
+			android:layout_marginEnd="@dimen/margin_large"
+			android:layout_marginRight="@dimen/margin_large"
+			android:background="@color/briar_accent"
+			android:src="@drawable/social_share"
+			android:tint="@color/action_bar_text"/>
+
+	</RelativeLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 5914fcef74..997d4d451d 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -4,6 +4,9 @@
 	<string name="nav_drawer_close_description">Close the navigation drawer</string>
 	<string name="app_name">Briar</string>
 	<string name="crash_report_title">Briar Crash Report</string>
+	<string name="describe_crash">Describe what happened</string>
+	<string name="optional_contact_email">Optional contact email</string>
+	<string name="could_not_load_crash_data">Could not load crash data.</string>
 	<string name="crash_report_saved">Crash report saved. It will be sent the next time you log into Briar.</string>
 	<string name="crash_report_not_saved">Could not save crash report to disk.</string>
 	<string name="ongoing_notification_title">Signed into Briar</string>
@@ -126,6 +129,10 @@
 	<string name="notify_sound_setting_default">Default ringtone</string>
 	<string name="notify_sound_setting_disabled">None</string>
 	<string name="choose_ringtone_title">Choose ringtone</string>
+	<string name="enable_acra_setting">Enable crash reporter</string>
+	<string name="acra_syslog_setting">Send system logs</string>
+	<string name="acra_user_email_setting">Optional contact email</string>
+	<string name="acra_alwaysaccept_setting">Always send reports</string>
 	<string name="panic_app_setting_title">Panic Button App</string>
 	<string name="panic_app_setting_summary">No app has been set</string>
 	<string name="panic_app_setting_none">None</string>
@@ -177,8 +184,8 @@
 	<string name="dialog_message_connect_panic_app">Are you sure that you want to allow %1$s to trigger destructive panic button actions?</string>
 	<string name="dialog_title_welcome">Welcome to Briar</string>
 	<string name="dialog_welcome_message">Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options.</string>
-	<string name="dialog_title_share_crash_report">Send to developers?</string>
-	<string name="dialog_message_share_crash_report">Would you like to send this crash report to the developers? It will be stored encrypted on your device until the next time you log into Briar, and then sent securely to the developers.</string>
+	<string name="dialog_title_share_crash_report">Briar has crashed</string>
+	<string name="dialog_message_share_crash_report">Would you like to review the crash report and send it to the developers? It will be stored encrypted on your device until the next time you log into Briar, and then sent securely to the developers.</string>
 	<string name="dialog_button_ok">OK</string>
 	<string name="dialog_button_introduce">Introduce</string>
 	<string name="dialog_button_accept">Accept</string>
diff --git a/briar-android/res/xml/settings.xml b/briar-android/res/xml/settings.xml
index 706d44ff92..e4b35f88a6 100644
--- a/briar-android/res/xml/settings.xml
+++ b/briar-android/res/xml/settings.xml
@@ -67,4 +67,33 @@
 
 	</PreferenceCategory>
 
+	<PreferenceCategory
+		android:title="Crash reports">
+
+		<CheckBoxPreference
+			android:defaultValue="true"
+			android:key="acra.enable"
+			android:title="@string/enable_acra_setting"/>
+
+		<CheckBoxPreference
+			android:defaultValue="true"
+			android:dependency="acra.enable"
+			android:key="acra.syslog.enable"
+			android:title="@string/acra_syslog_setting"/>
+
+		<EditTextPreference
+			android:dependency="acra.enable"
+			android:key="acra.user.email"
+			android:inputType="textEmailAddress"
+			android:summary=""
+			android:title="@string/acra_user_email_setting"/>
+
+		<CheckBoxPreference
+			android:defaultValue="false"
+			android:dependency="acra.enable"
+			android:key="acra.alwaysaccept"
+			android:title="@string/acra_alwaysaccept_setting"/>
+
+	</PreferenceCategory>
+
 </PreferenceScreen>
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index a3ced844d2..fcb53c41cb 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -21,6 +21,7 @@ import org.briarproject.android.keyagreement.KeyAgreementActivity;
 import org.briarproject.android.keyagreement.ShowQrCodeFragment;
 import org.briarproject.android.panic.PanicPreferencesActivity;
 import org.briarproject.android.panic.PanicResponderActivity;
+import org.briarproject.android.util.BriarReportSender;
 import org.briarproject.plugins.AndroidPluginsModule;
 import org.briarproject.system.AndroidSystemModule;
 
@@ -93,4 +94,6 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	// Eager singleton load
 	void inject(AppModule.EagerSingletons init);
+
+	void inject(BriarReportSender briarReportSender);
 }
diff --git a/briar-android/src/org/briarproject/android/BriarApplication.java b/briar-android/src/org/briarproject/android/BriarApplication.java
index dd32ded38e..0c46d4ad8c 100644
--- a/briar-android/src/org/briarproject/android/BriarApplication.java
+++ b/briar-android/src/org/briarproject/android/BriarApplication.java
@@ -3,11 +3,24 @@ package org.briarproject.android;
 import android.app.Application;
 import android.content.Context;
 
+import org.acra.ACRA;
+import org.acra.ReportingInteractionMode;
+import org.acra.annotation.ReportsCrashes;
 import org.briarproject.CoreModule;
+import org.briarproject.R;
+import org.briarproject.android.util.BriarReportPrimer;
 
-import java.lang.Thread.UncaughtExceptionHandler;
 import java.util.logging.Logger;
 
+@ReportsCrashes(
+		reportPrimerClass = BriarReportPrimer.class,
+		logcatArguments = {"-d", "-v", "time", "*:I"},
+		reportSenderFactoryClasses = {
+				org.briarproject.android.util.BriarReportSenderFactory.class},
+		mode = ReportingInteractionMode.DIALOG,
+		reportDialogClass = CrashReportActivity.class,
+		resDialogOkToast = R.string.crash_report_saved
+)
 public class BriarApplication extends Application {
 
 	private static final Logger LOG =
@@ -15,15 +28,18 @@ public class BriarApplication extends Application {
 
 	private AndroidComponent applicationComponent;
 
+	@Override
+	protected void attachBaseContext(Context base) {
+		super.attachBaseContext(base);
+
+		// The following line triggers the initialization of ACRA
+		ACRA.init(this);
+	}
+
 	@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);
 
 		applicationComponent = DaggerAndroidComponent.builder()
 				.appModule(new AppModule(this))
diff --git a/briar-android/src/org/briarproject/android/CrashHandler.java b/briar-android/src/org/briarproject/android/CrashHandler.java
deleted file mode 100644
index 8b2f53c6b3..0000000000
--- a/briar-android/src/org/briarproject/android/CrashHandler.java
+++ /dev/null
@@ -1,46 +0,0 @@
-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);
-		// Don't handle more than one exception
-		Thread.setDefaultUncaughtExceptionHandler(delegate);
-		// 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
index 923c2be08c..e93b7b53da 100644
--- a/briar-android/src/org/briarproject/android/CrashReportActivity.java
+++ b/briar-android/src/org/briarproject/android/CrashReportActivity.java
@@ -1,73 +1,58 @@
 package org.briarproject.android;
 
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.bluetooth.BluetoothAdapter;
 import android.content.DialogInterface;
-import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiManager;
+import android.content.SharedPreferences;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
-import android.os.Environment;
-import android.provider.Settings;
 import android.support.v7.app.AlertDialog;
-import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.TextView;
-import android.widget.Toast;
 
+import org.acra.ACRA;
+import org.acra.ACRAConstants;
+import org.acra.ReportField;
+import org.acra.collector.CrashReportData;
+import org.acra.dialog.BaseCrashReportDialog;
+import org.acra.file.CrashReportPersister;
+import org.acra.prefs.SharedPreferencesFactory;
 import org.briarproject.R;
-import org.briarproject.android.util.AndroidUtils;
 import org.briarproject.api.reporting.DevReporter;
-import org.briarproject.util.StringUtils;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Scanner;
 import java.util.logging.Logger;
-import java.util.regex.Pattern;
 
 import javax.inject.Inject;
 
-import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
-import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
-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.View.GONE;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 import static java.util.logging.Level.WARNING;
 
-public class CrashReportActivity extends AppCompatActivity
-		implements OnClickListener {
+public class CrashReportActivity extends BaseCrashReportDialog
+		implements DialogInterface.OnClickListener,
+		DialogInterface.OnCancelListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(CrashReportActivity.class.getName());
 
+	private static final String STATE_REVIEWING = "reviewing";
+
+	private SharedPreferencesFactory sharedPreferencesFactory;
+	private EditText userCommentView = null;
+	private EditText userEmailView = null;
 	private LinearLayout status = null;
 	private View progress = null;
 
 	@Inject
 	protected DevReporter reporter;
 
-	private volatile String stack = null;
-	private volatile int pid = -1;
-	private volatile BluetoothAdapter bt = null;
+	boolean reviewing;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -77,46 +62,78 @@ public class CrashReportActivity extends AppCompatActivity
 		((BriarApplication) getApplication()).getApplicationComponent()
 				.inject(this);
 
+		sharedPreferencesFactory =
+				new SharedPreferencesFactory(getApplicationContext(),
+						getConfig());
+
+		userCommentView = (EditText) findViewById(R.id.user_comment);
+		userEmailView = (EditText) findViewById(R.id.user_email);
 		status = (LinearLayout) findViewById(R.id.crash_status);
 		progress = findViewById(R.id.progress_wheel);
 
-		findViewById(R.id.share_crash_report).setOnClickListener(this);
+		findViewById(R.id.share_crash_report).setOnClickListener(
+				new OnClickListener() {
+					@Override
+					public void onClick(View v) {
+						processReport();
+					}
+				});
+
+		final SharedPreferences prefs = sharedPreferencesFactory.create();
+		String userEmail = prefs.getString(ACRA.PREF_USER_EMAIL_ADDRESS, "");
+		userEmailView.setText(userEmail);
 
-		Intent i = getIntent();
-		stack = i.getStringExtra("briar.STACK_TRACE");
-		pid = i.getIntExtra("briar.PID", -1);
-		bt = BluetoothAdapter.getDefaultAdapter();
+		if (state != null)
+			reviewing = state.getBoolean(STATE_REVIEWING, false);
 	}
 
 	@Override
 	public void onResume() {
 		super.onResume();
+		if (!reviewing) showDialog();
 		refresh();
 	}
 
+	@Override
+	public void onSaveInstanceState(Bundle state) {
+		super.onSaveInstanceState(state);
+		state.putBoolean(STATE_REVIEWING, reviewing);
+	}
+
 	@Override
 	public void onBackPressed() {
 		// show home screen, otherwise we are crashing again
-		Intent intent = new Intent(Intent.ACTION_MAIN);
-		intent.addCategory(Intent.CATEGORY_HOME);
-		startActivity(intent);
+		//Intent intent = new Intent(Intent.ACTION_MAIN);
+		//intent.addCategory(Intent.CATEGORY_HOME);
+		//startActivity(intent);
+		closeReport();
 	}
 
-	public void onClick(View view) {
-		// TODO Encapsulate the dialog in a re-usable fragment
+	@Override
+	public void onClick(DialogInterface dialog, int which) {
+		if (which == DialogInterface.BUTTON_POSITIVE) {
+			dialog.dismiss();
+		} else {
+			dialog.cancel();
+		}
+	}
+
+	@Override
+	public void onCancel(DialogInterface dialog) {
+		closeReport();
+	}
+
+	private void showDialog() {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this,
 				R.style.BriarDialogTheme);
-		builder.setTitle(R.string.dialog_title_share_crash_report);
-		builder.setMessage(R.string.dialog_message_share_crash_report);
-		builder.setNegativeButton(R.string.cancel_button, null);
-		builder.setPositiveButton(R.string.send,
-				new DialogInterface.OnClickListener() {
-					@Override
-					public void onClick(DialogInterface dialog, int which) {
-						saveCrashReport();
-					}
-				});
+		builder.setTitle(R.string.dialog_title_share_crash_report)
+				.setIcon(R.drawable.ic_warning_black_24dp)
+				.setMessage(R.string.dialog_message_share_crash_report)
+				.setPositiveButton(R.string.dialog_button_ok, this)
+				.setNegativeButton(R.string.cancel_button, this);
 		AlertDialog dialog = builder.create();
+		dialog.setCanceledOnTouchOutside(false);
+		dialog.setOnCancelListener(this);
 		dialog.show();
 	}
 
@@ -124,21 +141,40 @@ public class CrashReportActivity extends AppCompatActivity
 		status.setVisibility(INVISIBLE);
 		progress.setVisibility(VISIBLE);
 		status.removeAllViews();
-		new AsyncTask<Void, Void, Map<String, String>>() {
+		new AsyncTask<Void, Void, CrashReportData>() {
 
 			@Override
-			protected Map<String, String> doInBackground(Void... args) {
-				return getStatusMap();
+			protected CrashReportData doInBackground(Void... args) {
+				File reportFile = (File) getIntent().getSerializableExtra(
+						ACRAConstants.EXTRA_REPORT_FILE);
+				final CrashReportPersister persister =
+						new CrashReportPersister();
+				try {
+					return persister.load(reportFile);
+				} catch (IOException e) {
+					LOG.log(WARNING, "Could not load report file", e);
+					return null;
+				}
 			}
 
 			@Override
-			protected void onPostExecute(Map<String, String> result) {
-				for (Entry<String, String> e : result.entrySet()) {
-					View v = getLayoutInflater()
-							.inflate(R.layout.list_item_crash, status, false);
-					((TextView) v.findViewById(R.id.title)).setText(e.getKey());
-					((TextView) v.findViewById(R.id.content))
-							.setText(e.getValue());
+			protected void onPostExecute(CrashReportData crashData) {
+				LayoutInflater inflater = getLayoutInflater();
+				if (crashData != null) {
+					for (Entry<ReportField, String> e : crashData.entrySet()) {
+						View v = inflater.inflate(R.layout.list_item_crash,
+								status, false);
+						((TextView) v.findViewById(R.id.title))
+								.setText(e.getKey().toString());
+						((TextView) v.findViewById(R.id.content))
+								.setText(e.getValue());
+						status.addView(v);
+					}
+				} else {
+					View v = inflater.inflate(
+							android.R.layout.simple_list_item_1, status, false);
+					((TextView) v.findViewById(android.R.id.text1))
+							.setText(R.string.could_not_load_crash_data);
 					status.addView(v);
 				}
 				status.setVisibility(VISIBLE);
@@ -147,227 +183,28 @@ public class CrashReportActivity extends AppCompatActivity
 		}.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 architectures
-		Collection<String> abis = AndroidUtils.getSupportedArchitectures();
-		String joined = StringUtils.join(abis, ", ");
-		statusMap.put("Architecture:", joined);
-
-		// 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";
+	private void processReport() {
+		// Retrieve user comment
+		final String comment = userCommentView != null ?
+				userCommentView.getText().toString() : "";
+
+		// Store the user email
+		final String userEmail;
+		final SharedPreferences prefs = sharedPreferencesFactory.create();
+		if (userEmailView != null) {
+			userEmail = userEmailView.getText().toString();
+			final SharedPreferences.Editor prefEditor = prefs.edit();
+			prefEditor.putString(ACRA.PREF_USER_EMAIL_ADDRESS, userEmail);
+			prefEditor.commit();
 		} else {
-			systemMemory = (mem.availMem / 1024 / 1204) + " MiB free, "
-					+ (mem.threshold / 1024 / 1024) + " MiB threshold";
+			userEmail = prefs.getString(ACRA.PREF_USER_EMAIL_ADDRESS, "");
 		}
-		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
-		File root = Environment.getRootDirectory();
-		long rootTotal = root.getTotalSpace();
-		long rootFree = root.getFreeSpace();
-		String internal = (rootTotal / 1024 / 1024) + " MiB total, "
-				+ (rootFree / 1024 / 1024) + " MiB free";
-		statusMap.put("Internal storage:", internal);
-
-		// External storage (SD card)
-		File sd = Environment.getExternalStorageDirectory();
-		long sdTotal = sd.getTotalSpace();
-		long sdFree = sd.getFreeSpace();
-		String external = (sdTotal / 1024 / 1024) + " MiB total, "
-				+ (sdFree / 1024 / 1024) + " MiB free";
-		statusMap.put("External storage:", external);
-
-		// 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?
-		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();
-		try {
-			String btAddr = Settings.Secure.getString(getContentResolver(),
-					"bluetooth_address");
-			btStatus += "\nAddress from settings: " + btAddr;
-		} catch (SecurityException e) {
-			btStatus += "\nCould not get address from settings";
-		}
-		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);
+		sendCrash(comment, userEmail);
+		finish();
 	}
 
-	private void saveCrashReport() {
-		StringBuilder s = new StringBuilder();
-		for (Entry<String, String> e : getStatusMap().entrySet()) {
-			s.append(e.getKey());
-			s.append('\n');
-			s.append(e.getValue());
-			s.append("\n\n");
-		}
-		final String crashReport = s.toString();
-		try {
-			reporter.encryptCrashReportToFile(
-					AndroidUtils.getCrashReportDir(this), crashReport);
-			Toast.makeText(this, R.string.crash_report_saved, Toast.LENGTH_LONG)
-					.show();
-			finish();
-		} catch (FileNotFoundException e) {
-			if (LOG.isLoggable(WARNING))
-				LOG.log(WARNING, "Error while saving encrypted crash report",
-						e);
-			Toast.makeText(this, R.string.crash_report_not_saved,
-					Toast.LENGTH_SHORT).show();
-		}
+	private void closeReport() {
+		cancelReports();
+		finish();
 	}
 }
diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
index 155e50e2b7..e42e07e34b 100644
--- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java
+++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
@@ -15,6 +15,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 
 import static android.content.Context.MODE_PRIVATE;
@@ -24,7 +25,7 @@ public class AndroidUtils {
 	// Fake Bluetooth address returned by BluetoothAdapter on API 23 and later
 	private static final String FAKE_BLUETOOTH_ADDRESS = "02:00:00:00:00:00";
 
-	private static final String STORED_CRASH_REPORTS = "crash-reports";
+	private static final String STORED_REPORTS = "dev-reports";
 
 	@SuppressLint("NewApi")
 	@SuppressWarnings("deprecation")
@@ -89,7 +90,7 @@ public class AndroidUtils {
 		}
 	}
 
-	public static File getCrashReportDir(Context ctx) {
-		return ctx.getDir(STORED_CRASH_REPORTS, MODE_PRIVATE);
+	public static File getReportDir(Context ctx) {
+		return ctx.getDir(STORED_REPORTS, MODE_PRIVATE);
 	}
 }
diff --git a/briar-android/src/org/briarproject/android/util/BriarReportPrimer.java b/briar-android/src/org/briarproject/android/util/BriarReportPrimer.java
new file mode 100644
index 0000000000..67e96b3ca4
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/util/BriarReportPrimer.java
@@ -0,0 +1,184 @@
+package org.briarproject.android.util;
+
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.Settings;
+
+import org.acra.builder.ReportBuilder;
+import org.acra.builder.ReportPrimer;
+import org.briarproject.util.StringUtils;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.logging.Logger;
+
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+import static android.content.Context.ACTIVITY_SERVICE;
+import static android.content.Context.CONNECTIVITY_SERVICE;
+import static android.content.Context.WIFI_SERVICE;
+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 java.util.logging.Level.WARNING;
+
+public class BriarReportPrimer implements ReportPrimer {
+
+	private static final Logger LOG =
+			Logger.getLogger(BriarReportPrimer.class.getName());
+
+	@Override
+	public void primeReport(Context context, ReportBuilder builder) {
+		// System memory
+		Object o = context.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";
+		}
+		builder.customData("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";
+		builder.customData("Virtual machine memory", vmMemory);
+
+		// Internal storage
+		File root = Environment.getRootDirectory();
+		long rootTotal = root.getTotalSpace();
+		long rootFree = root.getFreeSpace();
+		String internal = (rootTotal / 1024 / 1024) + " MiB total, "
+				+ (rootFree / 1024 / 1024) + " MiB free";
+		builder.customData("Internal storage", internal);
+
+		// External storage (SD card)
+		File sd = Environment.getExternalStorageDirectory();
+		long sdTotal = sd.getTotalSpace();
+		long sdFree = sd.getFreeSpace();
+		String external = (sdTotal / 1024 / 1024) + " MiB total, "
+				+ (sdFree / 1024 / 1024) + " MiB free";
+		builder.customData("External storage", external);
+
+		// Is mobile data available?
+		o = context.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";
+		builder.customData("Mobile data status", mobileStatus);
+
+		// Is wifi available?
+		NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI);
+		boolean wifiAvailable = wifi != null && wifi.isAvailable();
+		// Is wifi enabled?
+		WifiManager wm = (WifiManager) context.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";
+		builder.customData("Wi-Fi status", wifiStatus);
+
+		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;
+				builder.customData("Wi-Fi address", address);
+			}
+		}
+
+		// Is Bluetooth available?
+		BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+		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";
+		builder.customData("Bluetooth status", btStatus);
+
+		if (bt != null) builder.customData("Bluetooth address", bt.getAddress());
+		String btSettingsAddr;
+		try {
+			btSettingsAddr = Settings.Secure.getString(context.getContentResolver(),
+					"bluetooth_address");
+		} catch (SecurityException e) {
+			btSettingsAddr = "Could not get address from settings";
+		}
+		builder.customData("Bluetooth address from settings", btSettingsAddr);
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/util/BriarReportSender.java b/briar-android/src/org/briarproject/android/util/BriarReportSender.java
new file mode 100644
index 0000000000..49507a6356
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/util/BriarReportSender.java
@@ -0,0 +1,50 @@
+package org.briarproject.android.util;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.acra.ReportField;
+import org.acra.collector.CrashReportData;
+import org.acra.sender.ReportSender;
+import org.acra.sender.ReportSenderException;
+import org.acra.util.JSONReportBuilder;
+import org.briarproject.android.AndroidComponent;
+import org.briarproject.api.reporting.DevReporter;
+
+import java.io.FileNotFoundException;
+
+import javax.inject.Inject;
+
+public class BriarReportSender implements ReportSender {
+
+	private final AndroidComponent component;
+
+	@Inject
+	protected DevReporter reporter;
+
+	public BriarReportSender(AndroidComponent component) {
+		this.component = component;
+	}
+
+	@Override
+	public void send(@NonNull Context context,
+			@NonNull CrashReportData errorContent)
+			throws ReportSenderException {
+		component.inject(this);
+
+		String crashReport;
+		try {
+			crashReport = errorContent.toJSON().toString();
+		} catch (JSONReportBuilder.JSONReportException e) {
+			throw new ReportSenderException("Couldn't create JSON", e);
+		}
+		try {
+			reporter.encryptCrashReportToFile(
+					AndroidUtils.getReportDir(context),
+					errorContent.getProperty(ReportField.REPORT_ID),
+					crashReport);
+		} catch (FileNotFoundException e) {
+			throw new ReportSenderException("Failed to encrypt report", e);
+		}
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/util/BriarReportSenderFactory.java b/briar-android/src/org/briarproject/android/util/BriarReportSenderFactory.java
new file mode 100644
index 0000000000..6dc106f743
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/util/BriarReportSenderFactory.java
@@ -0,0 +1,20 @@
+package org.briarproject.android.util;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.acra.config.ACRAConfiguration;
+import org.acra.sender.ReportSender;
+import org.acra.sender.ReportSenderFactory;
+import org.briarproject.android.BriarApplication;
+
+public class BriarReportSenderFactory implements ReportSenderFactory {
+	@NonNull
+	@Override
+	public ReportSender create(@NonNull Context context,
+			@NonNull ACRAConfiguration config) {
+		// ACRA passes in the Application as context
+		return new BriarReportSender(
+				((BriarApplication) context).getApplicationComponent());
+	}
+}
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
index 376a60ef59..0ba0a66604 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
@@ -366,7 +366,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
 			@Override
 			public void run() {
 				reporter.sendCrashReports(
-						AndroidUtils.getCrashReportDir(appContext), SOCKS_PORT);
+						AndroidUtils.getReportDir(appContext), SOCKS_PORT);
 			}
 		});
 	}
diff --git a/briar-api/src/org/briarproject/api/reporting/DevReporter.java b/briar-api/src/org/briarproject/api/reporting/DevReporter.java
index a2172b0f9d..6dc5f98023 100644
--- a/briar-api/src/org/briarproject/api/reporting/DevReporter.java
+++ b/briar-api/src/org/briarproject/api/reporting/DevReporter.java
@@ -15,8 +15,8 @@ public interface DevReporter {
 	 * @param crashReport    the crash report in the form expected by the server.
 	 * @throws FileNotFoundException if the report could not be written.
 	 */
-	void encryptCrashReportToFile(File crashReportDir, String crashReport)
-			throws FileNotFoundException;
+	void encryptCrashReportToFile(File crashReportDir, String filename,
+			String crashReport) throws FileNotFoundException;
 
 	/**
 	 * Send crash reports previously stored on-disk.
diff --git a/briar-core/src/org/briarproject/reporting/DevReporterImpl.java b/briar-core/src/org/briarproject/reporting/DevReporterImpl.java
index 192f10beec..7ca4b3b45d 100644
--- a/briar-core/src/org/briarproject/reporting/DevReporterImpl.java
+++ b/briar-core/src/org/briarproject/reporting/DevReporterImpl.java
@@ -33,8 +33,6 @@ class DevReporterImpl implements DevReporter {
 			Logger.getLogger(DevReporterImpl.class.getName());
 
 	private static final int SOCKET_TIMEOUT = 30 * 1000; // 30 seconds
-	private static final String PREFIX = "briar-";
-	private static final String REPORT_EXT = ".report";
 	private static final String CRLF = "\r\n";
 
 	private CryptoComponent crypto;
@@ -55,13 +53,12 @@ class DevReporterImpl implements DevReporter {
 	}
 
 	@Override
-	public void encryptCrashReportToFile(File crashReportDir,
+	public void encryptCrashReportToFile(File crashReportDir, String filename,
 			String crashReport) throws FileNotFoundException {
 		String encryptedReport =
 				crypto.encryptToKey(devConfig.getDevPublicKey(),
 						StringUtils.toUtf8(crashReport));
 
-		String filename = PREFIX + System.currentTimeMillis() + REPORT_EXT;
 		File report = new File(crashReportDir, filename);
 		PrintWriter writer = null;
 		try {
-- 
GitLab