refactor crash reporter. collect stats on crash; not send

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java            |   1 
src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java |   2 
src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java                    |  69 
src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java                     | 125 
4 files changed, 104 insertions(+), 93 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java 🔗

@@ -16,7 +16,7 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
     @Override
     public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
         setPreferencesFromResource(R.xml.preferences_main, rootKey);
-        final var about = findPreference("about");
+        final var about = findPreference("about2");
         final var connection = findPreference("connection");
         if (about == null || connection == null) {
             throw new IllegalStateException(

src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java 🔗

@@ -1,36 +1,59 @@
 package eu.siacs.conversations.utils;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import eu.siacs.conversations.BuildConfig;
+import eu.siacs.conversations.services.NotificationService;
+
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.io.Writer;
 import java.lang.Thread.UncaughtExceptionHandler;
-
-import eu.siacs.conversations.services.NotificationService;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
 
 public class ExceptionHandler implements UncaughtExceptionHandler {
 
-	private final UncaughtExceptionHandler defaultHandler;
-	private final Context context;
-
-	ExceptionHandler(final Context context) {
-		this.context = context;
-		this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
-	}
-
-	@Override
-	public void uncaughtException(@NonNull Thread thread, final Throwable throwable) {
-		NotificationService.cancelIncomingCallNotification(context);
-		final Writer stringWriter = new StringWriter();
-		final PrintWriter printWriter = new PrintWriter(stringWriter);
-		throwable.printStackTrace(printWriter);
-		final String stacktrace = stringWriter.toString();
-		printWriter.close();
-		ExceptionHelper.writeToStacktraceFile(context, stacktrace);
-		this.defaultHandler.uncaughtException(thread, throwable);
-	}
-
+    private static final SimpleDateFormat DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ENGLISH);
+
+    private final UncaughtExceptionHandler defaultHandler;
+    private final Context context;
+
+    ExceptionHandler(final Context context) {
+        this.context = context;
+        this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+    }
+
+    @Override
+    public void uncaughtException(@NonNull Thread thread, final Throwable throwable) {
+        NotificationService.cancelIncomingCallNotification(context);
+        final String stacktrace;
+        try (final StringWriter stringWriter = new StringWriter();
+                final PrintWriter printWriter = new PrintWriter(stringWriter)) {
+            throwable.printStackTrace(printWriter);
+            stacktrace = stringWriter.toString();
+        } catch (final IOException e) {
+            return;
+        }
+        final List<String> report =
+                ImmutableList.of(
+                        String.format(
+                                "Version: %s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME),
+                        String.format("Manufacturer: %s", Strings.nullToEmpty(Build.MANUFACTURER)),
+                        String.format("Device: %s", Strings.nullToEmpty(Build.DEVICE)),
+                        String.format("Timestamp: %s", DATE_FORMAT.format(new Date())),
+                        stacktrace);
+        ExceptionHelper.writeToStacktraceFile(context, Joiner.on("\n").join(report));
+        this.defaultHandler.uncaughtException(thread, throwable);
+    }
 }

src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java 🔗

@@ -1,12 +1,12 @@
 package eu.siacs.conversations.utils;
 
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.Signature;
 import android.util.Log;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Charsets;
+import com.google.common.io.CharSink;
+import com.google.common.io.Files;
 
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
@@ -17,19 +17,15 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.XmppActivity;
 
-import java.io.BufferedReader;
-import java.io.FileInputStream;
+import java.io.File;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.text.SimpleDateFormat;
-import java.util.Date;
 import java.util.Locale;
 
 public class ExceptionHelper {
 
     private static final String FILENAME = "stacktrace.txt";
-    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
 
     public static void init(final Context context) {
         if (Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler) {
@@ -39,72 +35,65 @@ public class ExceptionHelper {
     }
 
     public static boolean checkForCrash(final XmppActivity activity) {
+        final XmppConnectionService service =
+                activity == null ? null : activity.xmppConnectionService;
+        if (service == null) {
+            return false;
+        }
+        final AppSettings appSettings = new AppSettings(activity);
+        if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) {
+            return false;
+        }
+        final Account account = AccountUtils.getFirstEnabled(service);
+        if (account == null) {
+            return false;
+        }
+        final var file = new File(activity.getCacheDir(), FILENAME);
+        if (!file.exists()) {
+            return false;
+        }
+        final String report;
         try {
-            final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
-            if (service == null) {
-                return false;
-            }
-            final AppSettings appSettings = new AppSettings(activity);
-            if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) {
-                return false;
-            }
-            final Account account = AccountUtils.getFirstEnabled(service);
-            if (account == null) {
-                return false;
-            }
-            final FileInputStream file = activity.openFileInput(FILENAME);
-            final InputStreamReader inputStreamReader = new InputStreamReader(file);
-            final BufferedReader stacktrace = new BufferedReader(inputStreamReader);
-            final StringBuilder report = new StringBuilder();
-            final PackageManager pm = activity.getPackageManager();
-            final PackageInfo packageInfo;
-            try {
-                packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES);
-                final String versionName = packageInfo.versionName;
-                final int versionCode = packageInfo.versionCode;
-                final int version = versionCode > 10000 ? (versionCode / 100) : versionCode;
-                report.append(String.format(Locale.ROOT, "Version: %s(%d)", versionName, version)).append('\n');
-                report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n');
-                Signature[] signatures = packageInfo.signatures;
-                if (signatures != null && signatures.length >= 1) {
-                    report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n');
-                }
-                report.append('\n');
-            } catch (final Exception e) {
-                return false;
-            }
-            String line;
-            while ((line = stacktrace.readLine()) != null) {
-                report.append(line);
-                report.append('\n');
-            }
-            file.close();
-            activity.deleteFile(FILENAME);
-            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
-            builder.setTitle(activity.getString(R.string.crash_report_title, activity.getString(R.string.app_name)));
-            builder.setMessage(activity.getString(R.string.crash_report_message, activity.getString(R.string.app_name)));
-            builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> {
-
-                Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace");
-                Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
-                Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE);
-                service.sendMessage(message);
-            });
-            builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> appSettings.setSendCrashReports(false));
-            builder.create().show();
-            return true;
-        } catch (final IOException ignored) {
+            report = Files.asCharSource(file, Charsets.UTF_8).read();
+        } catch (final IOException e) {
             return false;
         }
+        if (file.delete()) {
+            Log.d(Config.LOGTAG, "deleted crash report file");
+        }
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
+        builder.setTitle(
+                activity.getString(
+                        R.string.crash_report_title, activity.getString(R.string.app_name)));
+        builder.setMessage(
+                activity.getString(
+                        R.string.crash_report_message, activity.getString(R.string.app_name)));
+        builder.setPositiveButton(
+                activity.getText(R.string.send_now),
+                (dialog, which) -> {
+                    Log.d(
+                            Config.LOGTAG,
+                            "using account="
+                                    + account.getJid().asBareJid()
+                                    + " to send in stack trace");
+                    Conversation conversation =
+                            service.findOrCreateConversation(
+                                    account, Config.BUG_REPORTS, false, true);
+                    Message message = new Message(conversation, report, Message.ENCRYPTION_NONE);
+                    service.sendMessage(message);
+                });
+        builder.setNegativeButton(
+                activity.getText(R.string.send_never),
+                (dialog, which) -> appSettings.setSendCrashReports(false));
+        builder.create().show();
+        return true;
     }
 
-    static void writeToStacktraceFile(Context context, String msg) {
+    static void writeToStacktraceFile(final Context context, final String msg) {
         try {
-            OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE);
-            os.write(msg.getBytes());
-            os.flush();
-            os.close();
-        } catch (IOException ignored) {
+            Files.asCharSink(new File(context.getCacheDir(), FILENAME), Charsets.UTF_8).write(msg);
+        } catch (IOException e) {
+            Log.w(Config.LOGTAG, "could not write stack trace to file", e);
         }
     }
 }