diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 89f3ece18..42c19dd04 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -127,7 +127,7 @@ public class GBApplication extends Application { private static SharedPreferences sharedPrefs; private static final String PREFS_VERSION = "shared_preferences_version"; //if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version - private static final int CURRENT_PREFS_VERSION = 41; + private static final int CURRENT_PREFS_VERSION = 42; private static final LimitedQueue mIDSenderLookup = new LimitedQueue<>(16); private static GBPrefs prefs; @@ -245,7 +245,7 @@ public class GBApplication extends Application { // the devicetype.json file //migrateDeviceTypes(); - setupExceptionHandler(); + setupExceptionHandler(prefs.getBoolean("crash_notification", isDebug())); Weather.getInstance().setCacheFile(getCacheDir(), prefs.getBoolean("cache_weather", true)); @@ -275,7 +275,7 @@ public class GBApplication extends Application { } GB.notify(NOTIFICATION_ID_ERROR, new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID) - .setSmallIcon(R.drawable.gadgetbridge_img) + .setSmallIcon(R.drawable.ic_notification) .setContentTitle(getString(R.string.error_background_service)) .setContentText(getString(R.string.error_background_service_reason_truncated)) .setStyle(new NotificationCompat.BigTextStyle() @@ -319,8 +319,8 @@ public class GBApplication extends Application { return logging.getLogPath(); } - private void setupExceptionHandler() { - LoggingExceptionHandler handler = new LoggingExceptionHandler(Thread.getDefaultUncaughtExceptionHandler()); + private void setupExceptionHandler(final boolean notifyOnCrash) { + final GBExceptionHandler handler = new GBExceptionHandler(Thread.getDefaultUncaughtExceptionHandler(), notifyOnCrash); Thread.setDefaultUncaughtExceptionHandler(handler); } @@ -515,7 +515,7 @@ public class GBApplication extends Application { } private static void saveAppsNotifBlackList() { - saveAppsNotifBlackList(sharedPrefs.edit()); + saveAppsNotifBlackList(sharedPrefs.edit()); } private static void saveAppsNotifBlackList(SharedPreferences.Editor editor) { @@ -574,7 +574,7 @@ public class GBApplication extends Application { } private static void saveAppsPebbleBlackList() { - saveAppsPebbleBlackList(sharedPrefs.edit()); + saveAppsPebbleBlackList(sharedPrefs.edit()); } private static void saveAppsPebbleBlackList(SharedPreferences.Editor editor) { @@ -659,7 +659,7 @@ public class GBApplication extends Application { DeviceType deviceType = DeviceType.fromName(dbDevice.getTypeName()); if (deviceTypes.contains(deviceType)) { - Log.i(TAG, "migrating global string preference " + globalPref + " for " + deviceType.name() + " " + dbDevice.getIdentifier() ); + Log.i(TAG, "migrating global string preference " + globalPref + " for " + deviceType.name() + " " + dbDevice.getIdentifier()); deviceSharedPrefsEdit.putString(perDevicePref, globalPrefValue); } deviceSharedPrefsEdit.apply(); @@ -685,7 +685,7 @@ public class GBApplication extends Application { DeviceType deviceType = DeviceType.fromName(dbDevice.getTypeName()); if (deviceTypes.contains(deviceType)) { - Log.i(TAG, "migrating global boolean preference " + globalPref + " for " + deviceType.name() + " " + dbDevice.getIdentifier() ); + Log.i(TAG, "migrating global boolean preference " + globalPref + " for " + deviceType.name() + " " + dbDevice.getIdentifier()); deviceSharedPrefsEdit.putBoolean(perDevicePref, globalPrefValue); } deviceSharedPrefsEdit.apply(); @@ -712,7 +712,7 @@ public class GBApplication extends Application { for (Device dbDevice : activeDevices) { String deviceTypeName = dbDevice.getTypeName(); - if(deviceTypeName.isEmpty() || deviceTypeName.equals("UNKNOWN")){ + if (deviceTypeName.isEmpty() || deviceTypeName.equals("UNKNOWN")) { deviceTypeName = deviceIdNameMapping.optString( String.valueOf(dbDevice.getType()), "UNKNOWN" @@ -730,7 +730,7 @@ public class GBApplication extends Application { SharedPreferences.Editor editor = sharedPrefs.edit(); // this comes before all other migrations since the new column DeviceTypeName was added as non-null - if (oldVersion < 25){ + if (oldVersion < 25) { migrateDeviceTypes(); } @@ -892,8 +892,8 @@ public class GBApplication extends Application { DeviceType deviceType = DeviceType.fromName(dbDevice.getTypeName()); if (deviceType == MIBAND) { - int deviceTimeOffsetHours = deviceSharedPrefs.getInt("device_time_offset_hours",0); - deviceSharedPrefsEdit.putString("device_time_offset_hours", Integer.toString(deviceTimeOffsetHours) ); + int deviceTimeOffsetHours = deviceSharedPrefs.getInt("device_time_offset_hours", 0); + deviceSharedPrefsEdit.putString("device_time_offset_hours", Integer.toString(deviceTimeOffsetHours)); } deviceSharedPrefsEdit.apply(); @@ -996,7 +996,7 @@ public class GBApplication extends Application { try (DBHandler db = acquireDB()) { DaoSession daoSession = db.getDaoSession(); List activeDevices = DBHelper.getActiveDevices(daoSession); - migrateBooleanPrefToPerDevicePref("transliteration", false, "pref_transliteration_enabled", (ArrayList)activeDevices); + migrateBooleanPrefToPerDevicePref("transliteration", false, "pref_transliteration_enabled", (ArrayList) activeDevices); Log.w(TAG, "migrating transliteration settings"); } catch (Exception e) { Log.e(TAG, "Failed to migrate prefs to version 9", e); @@ -1831,6 +1831,13 @@ public class GBApplication extends Application { } } + if (oldVersion < 42) { + // Enable crash notification by default on debug builds + if (!prefs.contains("crash_notification")) { + editor.putBoolean("crash_notification", isDebug()); + } + } + editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); editor.apply(); } @@ -1953,6 +1960,7 @@ public class GBApplication extends Application { } public static boolean isNightly() { + //noinspection ConstantValue - false positive return BuildConfig.APPLICATION_ID.contains("nightly"); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBExceptionHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBExceptionHandler.java new file mode 100644 index 000000000..4f74e62b7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBExceptionHandler.java @@ -0,0 +1,103 @@ +/* Copyright (C) 2015-2024 Carsten Pfeiffer, José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge; + + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID; +import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_ID_ERROR; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Random; + +import ch.qos.logback.classic.LoggerContext; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils; + +/** + * Catches otherwise uncaught exceptions, logs them and terminates the app. + */ +public class GBExceptionHandler implements Thread.UncaughtExceptionHandler { + private static final Logger LOG = LoggerFactory.getLogger(GBExceptionHandler.class); + private final Thread.UncaughtExceptionHandler mDelegate; + private final boolean mNotifyOnCrash; + + public GBExceptionHandler(Thread.UncaughtExceptionHandler delegate, final boolean notifyOnCrash) { + mDelegate = delegate; + mNotifyOnCrash = notifyOnCrash; + } + + @Override + public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) { + LOG.error("Uncaught exception", ex); + // flush the log buffers and stop logging + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + + if (mNotifyOnCrash) { + showNotification(ex); + } + + if (mDelegate != null) { + mDelegate.uncaughtException(thread, ex); + } else { + System.exit(1); + } + } + + private void showNotification(final Throwable e) { + final Context context = GBApplication.getContext(); + + final Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, Log.getStackTraceString(e)); + shareIntent.setType("text/plain"); + + final PendingIntent pendingShareIntent = PendingIntentUtils.getActivity( + context, + 0, + Intent.createChooser(shareIntent, context.getString(R.string.app_crash_share_stacktrace)), + PendingIntent.FLAG_UPDATE_CURRENT, + false + ); + + final NotificationCompat.Action shareAction = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_share, context.getString(R.string.share), pendingShareIntent).build(); + + final Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(context.getString( + R.string.app_crash_notification_title, + context.getString(R.string.app_name) + )) + .setContentText(e.getLocalizedMessage()) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .addAction(shareAction) + .build(); + + GB.notify(NOTIFICATION_ID_ERROR, notification, context); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LoggingExceptionHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LoggingExceptionHandler.java deleted file mode 100644 index 80ee5e743..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LoggingExceptionHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright (C) 2015-2024 Carsten Pfeiffer - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge; - - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.qos.logback.classic.LoggerContext; - -/** - * Catches otherwise uncaught exceptions, logs them and terminates the app. - */ -public class LoggingExceptionHandler implements Thread.UncaughtExceptionHandler { - private static final Logger LOG = LoggerFactory.getLogger(LoggingExceptionHandler.class); - private final Thread.UncaughtExceptionHandler mDelegate; - - public LoggingExceptionHandler(Thread.UncaughtExceptionHandler delegate) { - mDelegate = delegate; - } - - @Override - public void uncaughtException(Thread thread, Throwable ex) { - LOG.error("Uncaught exception: " + ex.getMessage(), ex); - // flush the log buffers and stop logging - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.stop(); - - if (mDelegate != null) { - mDelegate.uncaughtException(thread, ex); - } else { - System.exit(1); - } - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5357dde7a..3daf0b7c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2113,6 +2113,10 @@ Failed to start background service Starting the background service failed because… + Notify on crash + When the app crashes, display a notification with the error + %1s has crashed + Share error ALREADY BONDED KEY REQUIRED, LONG PRESS TO ENTER UNSUPPORTED diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index ec661038e..b5729f5f5 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -364,6 +364,13 @@ android:layout="@layout/preference_checkbox" android:title="@string/pref_write_logfiles" app:iconSpaceReserved="false" /> +