diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 7e3632ac7..6cd2adce3 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -17,6 +17,7 @@
+
+
+
+
+
+
Briar
+ Briar Crash Report
Signed into Briar
Touch to show the dashboard.
Briar Setup
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 000000000..8e54544cf
--- /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 000000000..757f999b2
--- /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 000000000..9d8520a87
--- /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>() {
+
+ protected Map doInBackground(Void... args) {
+ return getStatusMap();
+ }
+
+ protected void onPostExecute(Map result) {
+ Context ctx = CrashReportActivity.this;
+ int pad = LayoutUtils.getPadding(ctx);
+ for(Entry 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 getStatusMap() {
+ Map statusMap = new LinkedHashMap();
+
+ // 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() {
+ 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>() {
+
+ protected Map doInBackground(Void... args) {
+ return getStatusMap();
+ }
+
+ protected void onPostExecute(Map 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 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 4d053d32c..e4ad6f864 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 1574c501a..829168766 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")