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")