From f73f0aa4ab28353941887ef33137fdbd09d4eb11 Mon Sep 17 00:00:00 2001 From: str4d 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 | 121 ++++-- 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 | 399 ++++++------------ .../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, 546 insertions(+), 387 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 dc0874e3b..51811047b 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -2,11 +2,11 @@ - - - - + android:process=":briar_error_handler"> + + diff --git a/briar-android/res/layout/activity_crash.xml b/briar-android/res/layout/activity_crash.xml index e412ec374..bf68dc87a 100644 --- a/briar-android/res/layout/activity_crash.xml +++ b/briar-android/res/layout/activity_crash.xml @@ -1,47 +1,98 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + - + android:layout_centerInParent="true" + android:indeterminate="true"/> - + - - - \ No newline at end of file + + \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 5914fcef7..997d4d451 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -4,6 +4,9 @@ Close the navigation drawer Briar Briar Crash Report + Describe what happened + Optional contact email + Could not load crash data. Crash report saved. It will be sent the next time you log into Briar. Could not save crash report to disk. Signed into Briar @@ -126,6 +129,10 @@ Default ringtone None Choose ringtone + Enable crash reporter + Send system logs + Optional contact email + Always send reports Panic Button App No app has been set None @@ -177,8 +184,8 @@ Are you sure that you want to allow %1$s to trigger destructive panic button actions? Welcome to Briar Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options. - Send to developers? - 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. + Briar has crashed + 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. OK Introduce Accept diff --git a/briar-android/res/xml/settings.xml b/briar-android/res/xml/settings.xml index 706d44ff9..e4b35f88a 100644 --- a/briar-android/res/xml/settings.xml +++ b/briar-android/res/xml/settings.xml @@ -67,4 +67,33 @@ + + + + + + + + + + + + \ 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 a3ced844d..fcb53c41c 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 dd32ded38..0c46d4ad8 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 8b2f53c6b..000000000 --- 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 923c2be08..e93b7b53d 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(); + } + }); - Intent i = getIntent(); - stack = i.getStringExtra("briar.STACK_TRACE"); - pid = i.getIntExtra("briar.PID", -1); - bt = BluetoothAdapter.getDefaultAdapter(); + final SharedPreferences prefs = sharedPreferencesFactory.create(); + String userEmail = prefs.getString(ACRA.PREF_USER_EMAIL_ADDRESS, ""); + userEmailView.setText(userEmail); + + 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>() { + new AsyncTask() { @Override - protected Map 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 result) { - for (Entry 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 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 getStatusMap() { - Map statusMap = new LinkedHashMap(); + private void processReport() { + // Retrieve user comment + final String comment = userCommentView != null ? + userCommentView.getText().toString() : ""; - // 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 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"; + // 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); + sendCrash(comment, userEmail); + finish(); } - 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 saveCrashReport() { - StringBuilder s = new StringBuilder(); - for (Entry 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 155e50e2b..e42e07e34 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 000000000..67e96b3ca --- /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 000000000..49507a635 --- /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 000000000..6dc106f74 --- /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 376a60ef5..0ba0a6660 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 a2172b0f9..6dc5f9802 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 192f10bee..7ca4b3b45 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 {