diff --git a/app/src/main/java/eu/faircode/email/ApplicationEx.java b/app/src/main/java/eu/faircode/email/ApplicationEx.java index 13a2c9061f..54b2fe7ba3 100644 --- a/app/src/main/java/eu/faircode/email/ApplicationEx.java +++ b/app/src/main/java/eu/faircode/email/ApplicationEx.java @@ -19,7 +19,6 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ -import android.app.ActivityManager; import android.app.Application; import android.app.Notification; import android.app.NotificationChannel; @@ -28,36 +27,11 @@ import android.app.NotificationManager; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; -import android.os.Build; -import android.os.DeadSystemException; -import android.os.RemoteException; -import android.view.OrientationEventListener; import android.webkit.CookieManager; -import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; -import com.bugsnag.android.BeforeNotify; -import com.bugsnag.android.BeforeSend; -import com.bugsnag.android.Bugsnag; -import com.bugsnag.android.Client; -import com.bugsnag.android.Error; -import com.bugsnag.android.Report; -import com.sun.mail.iap.ProtocolException; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; import java.util.Locale; -import java.util.UUID; -import java.util.concurrent.TimeoutException; - -import javax.mail.MessagingException; public class ApplicationEx extends Application { private Thread.UncaughtExceptionHandler prev = null; @@ -71,7 +45,7 @@ public class ApplicationEx extends Application { public void onCreate() { super.onCreate(); - logMemory("App create version=" + BuildConfig.VERSION_NAME); + Log.logMemory(this, "App create version=" + BuildConfig.VERSION_NAME); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final boolean crash_reports = prefs.getBoolean("crash_reports", false); @@ -81,12 +55,12 @@ public class ApplicationEx extends Application { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { - if (!crash_reports && ownFault(ex)) { + if (!crash_reports && Log.ownFault(ex)) { Log.e(ex); if (BuildConfig.BETA_RELEASE || !Helper.isPlayStoreInstall(ApplicationEx.this)) - writeCrashLog(ApplicationEx.this, ex); + Log.writeCrash(ApplicationEx.this, ex); if (prev != null) prev.uncaughtException(thread, ex); @@ -97,7 +71,7 @@ public class ApplicationEx extends Application { } }); - setupBugsnag(); + Log.setupBugsnag(this); upgrade(this); @@ -115,156 +89,16 @@ public class ApplicationEx extends Application { @Override public void onTrimMemory(int level) { - logMemory("Trim memory level=" + level); + Log.logMemory(this, "Trim memory level=" + level); super.onTrimMemory(level); } @Override public void onLowMemory() { - logMemory("Low memory"); + Log.logMemory(this, "Low memory"); super.onLowMemory(); } - private void logMemory(String message) { - ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); - ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); - activityManager.getMemoryInfo(mi); - int mb = Math.round(mi.availMem / 0x100000L); - int perc = Math.round(mi.availMem / (float) mi.totalMem * 100.0f); - Log.i(message + " " + mb + " MB" + " " + perc + " %"); - } - - private void setupBugsnag() { - // https://docs.bugsnag.com/platforms/android/sdk/ - com.bugsnag.android.Configuration config = - new com.bugsnag.android.Configuration("9d2d57476a0614974449a3ec33f2604a"); - - if (BuildConfig.DEBUG) - config.setReleaseStage("debug"); - else { - String type = "other"; - if (Helper.hasValidFingerprint(this)) - if (BuildConfig.PLAY_STORE_RELEASE) - type = "play"; - else - type = "full"; - config.setReleaseStage(type + (BuildConfig.BETA_RELEASE ? "/beta" : "")); - } - - config.setAutoCaptureSessions(false); - - config.setDetectAnrs(false); - config.setDetectNdkCrashes(false); - - List ignore = new ArrayList<>(); - - ignore.add("com.sun.mail.util.MailConnectException"); - - ignore.add("android.accounts.OperationCanceledException"); - ignore.add("android.app.RemoteServiceException"); - - ignore.add("java.lang.NoClassDefFoundError"); - ignore.add("java.lang.UnsatisfiedLinkError"); - - ignore.add("java.nio.charset.MalformedInputException"); - - ignore.add("java.net.ConnectException"); - ignore.add("java.net.SocketException"); - ignore.add("java.net.SocketTimeoutException"); - ignore.add("java.net.UnknownHostException"); - - ignore.add("javax.mail.AuthenticationFailedException"); - ignore.add("javax.mail.FolderClosedException"); - ignore.add("javax.mail.internet.AddressException"); - ignore.add("javax.mail.MessageRemovedException"); - ignore.add("javax.mail.ReadOnlyFolderException"); - ignore.add("javax.mail.StoreClosedException"); - - ignore.add("org.xmlpull.v1.XmlPullParserException"); - - config.setIgnoreClasses(ignore.toArray(new String[0])); - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - config.beforeSend(new BeforeSend() { - @Override - public boolean run(@NonNull Report report) { - Error error = report.getError(); - if (error != null) { - Throwable ex = error.getException(); - - if (ex instanceof MessagingException && - (ex.getCause() instanceof IOException || - ex.getCause() instanceof ProtocolException)) - // IOException includes SocketException, SocketTimeoutException - // ProtocolException includes ConnectionException - return false; - - if (ex instanceof MessagingException && - ("connection failure".equals(ex.getMessage()) || - "failed to create new store connection".equals(ex.getMessage()) || - "Failed to fetch headers".equals(ex.getMessage()) || - "Failed to load IMAP envelope".equals(ex.getMessage()) || - "Unable to load BODYSTRUCTURE".equals(ex.getMessage()))) - return false; - - if (ex instanceof IllegalStateException && - ("Not connected".equals(ex.getMessage()) || - "This operation is not allowed on a closed folder".equals(ex.getMessage()))) - return false; - - if (ex instanceof FileNotFoundException && - ex.getMessage() != null && - (ex.getMessage().startsWith("Download image failed") || - ex.getMessage().startsWith("https://ipinfo.io/") || - ex.getMessage().startsWith("https://autoconfig.thunderbird.net/"))) - return false; - } - - return prefs.getBoolean("crash_reports", false); // opt-in - } - }); - - Bugsnag.init(this, config); - - Client client = Bugsnag.getClient(); - - try { - Log.i("Disabling orientation listener"); - Field fOrientationListener = Client.class.getDeclaredField("orientationListener"); - fOrientationListener.setAccessible(true); - OrientationEventListener orientationListener = (OrientationEventListener) fOrientationListener.get(client); - orientationListener.disable(); - Log.i("Disabled orientation listener"); - } catch (Throwable ex) { - Log.e(ex); - } - - String uuid = prefs.getString("uuid", null); - if (uuid == null) { - uuid = UUID.randomUUID().toString(); - prefs.edit().putString("uuid", uuid).apply(); - } - Log.i("uuid=" + uuid); - client.setUserId(uuid); - - if (prefs.getBoolean("crash_reports", false)) - Bugsnag.startSession(); - - final String installer = getPackageManager().getInstallerPackageName(BuildConfig.APPLICATION_ID); - final boolean fingerprint = Helper.hasValidFingerprint(this); - - Bugsnag.beforeNotify(new BeforeNotify() { - @Override - public boolean run(@NonNull Error error) { - error.addToTab("extra", "installer", installer == null ? "-" : installer); - error.addToTab("extra", "fingerprint", fingerprint); - error.addToTab("extra", "free", Log.getFreeMemMb()); - return true; - } - }); - } - static void upgrade(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int version = prefs.getInt("version", BuildConfig.VERSION_CODE); @@ -304,18 +138,6 @@ public class ApplicationEx extends Application { editor.apply(); } - static Context getLocalizedContext(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean english = prefs.getBoolean("english", false); - - if (english) { - Configuration config = new Configuration(context.getResources().getConfiguration()); - config.setLocale(Locale.US); - return context.createConfigurationContext(config); - } else - return context; - } - private void createNotificationChannels() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); @@ -378,61 +200,15 @@ public class ApplicationEx extends Application { } } - public boolean ownFault(Throwable ex) { - if (ex instanceof OutOfMemoryError) - return false; + static Context getLocalizedContext(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean english = prefs.getBoolean("english", false); - if (ex instanceof RemoteException) - return false; - - /* - java.lang.NoSuchMethodError: No direct method ()V in class Landroid/security/IKeyChainService$Stub; or its super classes (declaration of 'android.security.IKeyChainService$Stub' appears in /system/framework/framework.jar!classes2.dex) - java.lang.NoSuchMethodError: No direct method ()V in class Landroid/security/IKeyChainService$Stub; or its super classes (declaration of 'android.security.IKeyChainService$Stub' appears in /system/framework/framework.jar!classes2.dex) - at com.android.keychain.KeyChainService$1.(KeyChainService.java:95) - at com.android.keychain.KeyChainService.(KeyChainService.java:95) - at java.lang.Class.newInstance(Native Method) - at android.app.AppComponentFactory.instantiateService(AppComponentFactory.java:103) - */ - if (ex instanceof NoSuchMethodError) - return false; - - if (ex.getMessage() != null && - (ex.getMessage().startsWith("Bad notification posted") || - ex.getMessage().contains("ActivityRecord not found") || - ex.getMessage().startsWith("Unable to create layer"))) - return false; - - if (ex instanceof TimeoutException && - ex.getMessage() != null && - ex.getMessage().startsWith("com.sun.mail.imap.IMAPStore.finalize")) - return false; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - if (ex instanceof RuntimeException && ex.getCause() instanceof DeadSystemException) - return false; - - if (BuildConfig.BETA_RELEASE) - return true; - - while (ex != null) { - for (StackTraceElement ste : ex.getStackTrace()) - if (ste.getClassName().startsWith(getPackageName())) - return true; - ex = ex.getCause(); - } - - return false; - } - - static void writeCrashLog(Context context, Throwable ex) { - File file = new File(context.getCacheDir(), "crash.log"); - Log.w("Writing exception to " + file); - - try (FileWriter out = new FileWriter(file, true)) { - out.write(BuildConfig.VERSION_NAME + " " + new Date() + "\r\n"); - out.write(ex + "\r\n" + android.util.Log.getStackTraceString(ex) + "\r\n"); - } catch (IOException e) { - Log.e(e); - } + if (english) { + Configuration config = new Configuration(context.getResources().getConfiguration()); + config.setLocale(Locale.US); + return context.createConfigurationContext(config); + } else + return context; } } diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 555f8e3002..2131ab31b2 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -105,7 +105,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import com.bugsnag.android.Bugsnag; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; @@ -4510,7 +4509,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. prefs.edit().putBoolean("crash_reports", true).apply(); if (cbNotAgain.isChecked()) prefs.edit().putBoolean("crash_reports_asked", true).apply(); - Bugsnag.startSession(); + Log.setCrashReporting(true); } }) .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java index dee6a4380f..bd9ee1a397 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java @@ -44,8 +44,6 @@ import androidx.appcompat.widget.SwitchCompat; import androidx.constraintlayout.widget.Group; import androidx.preference.PreferenceManager; -import com.bugsnag.android.Bugsnag; - public class FragmentOptionsMisc extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener { private SwitchCompat swBadge; private SwitchCompat swSubscriptions; @@ -190,10 +188,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc .remove("crash_reports_asked") .putBoolean("crash_reports", checked) .apply(); - if (checked) - Bugsnag.startSession(); - else - Bugsnag.stopSession(); + Log.setCrashReporting(checked); } }); diff --git a/app/src/main/java/eu/faircode/email/Log.java b/app/src/main/java/eu/faircode/email/Log.java index 2a0fe782bd..19ce822007 100644 --- a/app/src/main/java/eu/faircode/email/Log.java +++ b/app/src/main/java/eu/faircode/email/Log.java @@ -31,17 +31,27 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.os.Build; import android.os.Bundle; +import android.os.DeadSystemException; import android.os.Debug; import android.os.PowerManager; +import android.os.RemoteException; import android.text.TextUtils; import android.view.Display; +import android.view.OrientationEventListener; import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; +import com.bugsnag.android.BeforeNotify; +import com.bugsnag.android.BeforeSend; import com.bugsnag.android.BreadcrumbType; import com.bugsnag.android.Bugsnag; +import com.bugsnag.android.Client; +import com.bugsnag.android.Error; +import com.bugsnag.android.Report; import com.bugsnag.android.Severity; +import com.sun.mail.iap.ProtocolException; import org.json.JSONException; import org.json.JSONObject; @@ -49,19 +59,26 @@ import org.json.JSONObject; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.text.DateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeoutException; import javax.mail.Address; +import javax.mail.MessagingException; import javax.mail.Part; import javax.mail.internet.InternetAddress; @@ -118,8 +135,143 @@ public class Log { return android.util.Log.e(TAG, prefix + " " + ex + "\n" + android.util.Log.getStackTraceString(ex)); } + static void setCrashReporting(boolean enabled) { + if (enabled) + Bugsnag.startSession(); + else + Bugsnag.stopSession(); + } + static void breadcrumb(String name, Map crumb) { - Bugsnag.leaveBreadcrumb("operation", BreadcrumbType.LOG, crumb); + Bugsnag.leaveBreadcrumb(name, BreadcrumbType.LOG, crumb); + } + + static void setupBugsnag(Context context) { + // https://docs.bugsnag.com/platforms/android/sdk/ + com.bugsnag.android.Configuration config = + new com.bugsnag.android.Configuration("9d2d57476a0614974449a3ec33f2604a"); + + if (BuildConfig.DEBUG) + config.setReleaseStage("debug"); + else { + String type = "other"; + if (Helper.hasValidFingerprint(context)) + if (BuildConfig.PLAY_STORE_RELEASE) + type = "play"; + else + type = "full"; + config.setReleaseStage(type + (BuildConfig.BETA_RELEASE ? "/beta" : "")); + } + + config.setAutoCaptureSessions(false); + + config.setDetectAnrs(false); + config.setDetectNdkCrashes(false); + + List ignore = new ArrayList<>(); + + ignore.add("com.sun.mail.util.MailConnectException"); + + ignore.add("android.accounts.OperationCanceledException"); + ignore.add("android.app.RemoteServiceException"); + + ignore.add("java.lang.NoClassDefFoundError"); + ignore.add("java.lang.UnsatisfiedLinkError"); + + ignore.add("java.nio.charset.MalformedInputException"); + + ignore.add("java.net.ConnectException"); + ignore.add("java.net.SocketException"); + ignore.add("java.net.SocketTimeoutException"); + ignore.add("java.net.UnknownHostException"); + + ignore.add("javax.mail.AuthenticationFailedException"); + ignore.add("javax.mail.FolderClosedException"); + ignore.add("javax.mail.internet.AddressException"); + ignore.add("javax.mail.MessageRemovedException"); + ignore.add("javax.mail.ReadOnlyFolderException"); + ignore.add("javax.mail.StoreClosedException"); + + ignore.add("org.xmlpull.v1.XmlPullParserException"); + + config.setIgnoreClasses(ignore.toArray(new String[0])); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + config.beforeSend(new BeforeSend() { + @Override + public boolean run(@NonNull Report report) { + Throwable ex = report.getError().getException(); + + if (ex instanceof MessagingException && + (ex.getCause() instanceof IOException || + ex.getCause() instanceof ProtocolException)) + // IOException includes SocketException, SocketTimeoutException + // ProtocolException includes ConnectionException + return false; + + if (ex instanceof MessagingException && + ("connection failure".equals(ex.getMessage()) || + "failed to create new store connection".equals(ex.getMessage()) || + "Failed to fetch headers".equals(ex.getMessage()) || + "Failed to load IMAP envelope".equals(ex.getMessage()) || + "Unable to load BODYSTRUCTURE".equals(ex.getMessage()))) + return false; + + if (ex instanceof IllegalStateException && + ("Not connected".equals(ex.getMessage()) || + "This operation is not allowed on a closed folder".equals(ex.getMessage()))) + return false; + + if (ex instanceof FileNotFoundException && + ex.getMessage() != null && + (ex.getMessage().startsWith("Download image failed") || + ex.getMessage().startsWith("https://ipinfo.io/") || + ex.getMessage().startsWith("https://autoconfig.thunderbird.net/"))) + return false; + + return prefs.getBoolean("crash_reports", false); // opt-in + } + }); + + Bugsnag.init(context, config); + + Client client = Bugsnag.getClient(); + + try { + Log.i("Disabling orientation listener"); + Field fOrientationListener = Client.class.getDeclaredField("orientationListener"); + fOrientationListener.setAccessible(true); + OrientationEventListener orientationListener = (OrientationEventListener) fOrientationListener.get(client); + orientationListener.disable(); + Log.i("Disabled orientation listener"); + } catch (Throwable ex) { + Log.e(ex); + } + + String uuid = prefs.getString("uuid", null); + if (uuid == null) { + uuid = UUID.randomUUID().toString(); + prefs.edit().putString("uuid", uuid).apply(); + } + Log.i("uuid=" + uuid); + client.setUserId(uuid); + + if (prefs.getBoolean("crash_reports", false)) + Bugsnag.startSession(); + + final String installer = context.getPackageManager().getInstallerPackageName(BuildConfig.APPLICATION_ID); + final boolean fingerprint = Helper.hasValidFingerprint(context); + + Bugsnag.beforeNotify(new BeforeNotify() { + @Override + public boolean run(@NonNull Error error) { + error.addToTab("extra", "installer", installer == null ? "-" : installer); + error.addToTab("extra", "fingerprint", fingerprint); + error.addToTab("extra", "free", Log.getFreeMemMb()); + return true; + } + }); } static void logExtras(Intent intent) { @@ -161,6 +313,73 @@ public class Log { } } + static void logMemory(Context context, String message) { + ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + activityManager.getMemoryInfo(mi); + int mb = Math.round(mi.availMem / 0x100000L); + int perc = Math.round(mi.availMem / (float) mi.totalMem * 100.0f); + Log.i(message + " " + mb + " MB" + " " + perc + " %"); + } + + static boolean ownFault(Throwable ex) { + if (ex instanceof OutOfMemoryError) + return false; + + if (ex instanceof RemoteException) + return false; + + /* + java.lang.NoSuchMethodError: No direct method ()V in class Landroid/security/IKeyChainService$Stub; or its super classes (declaration of 'android.security.IKeyChainService$Stub' appears in /system/framework/framework.jar!classes2.dex) + java.lang.NoSuchMethodError: No direct method ()V in class Landroid/security/IKeyChainService$Stub; or its super classes (declaration of 'android.security.IKeyChainService$Stub' appears in /system/framework/framework.jar!classes2.dex) + at com.android.keychain.KeyChainService$1.(KeyChainService.java:95) + at com.android.keychain.KeyChainService.(KeyChainService.java:95) + at java.lang.Class.newInstance(Native Method) + at android.app.AppComponentFactory.instantiateService(AppComponentFactory.java:103) + */ + if (ex instanceof NoSuchMethodError) + return false; + + if (ex.getMessage() != null && + (ex.getMessage().startsWith("Bad notification posted") || + ex.getMessage().contains("ActivityRecord not found") || + ex.getMessage().startsWith("Unable to create layer"))) + return false; + + if (ex instanceof TimeoutException && + ex.getMessage() != null && + ex.getMessage().startsWith("com.sun.mail.imap.IMAPStore.finalize")) + return false; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + if (ex instanceof RuntimeException && ex.getCause() instanceof DeadSystemException) + return false; + + if (BuildConfig.BETA_RELEASE) + return true; + + while (ex != null) { + for (StackTraceElement ste : ex.getStackTrace()) + if (ste.getClassName().startsWith(BuildConfig.APPLICATION_ID)) + return true; + ex = ex.getCause(); + } + + return false; + } + + static void writeCrash(Context context, Throwable ex) { + File file = new File(context.getCacheDir(), "crash.log"); + Log.w("Writing exception to " + file); + + try (FileWriter out = new FileWriter(file, true)) { + out.write(BuildConfig.VERSION_NAME + " " + new Date() + "\r\n"); + out.write(ex + "\r\n" + android.util.Log.getStackTraceString(ex) + "\r\n"); + } catch (IOException e) { + Log.e(e); + } + } + static EntityMessage getDebugInfo(Context context, int title, Throwable ex, String log) throws IOException { StringBuilder sb = new StringBuilder(); sb.append(context.getString(title)).append("\n\n\n\n");