diff --git a/.gitignore b/.gitignore index eee2dba424..0ce2c01344 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ # Sentry Config File sentry.properties /opencode.json +# JVM crash and replay logs +hs_err_pid*.log +replay_pid*.log diff --git a/app/build.gradle b/app/build.gradle index 773cd86c77..281cfb9b66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,17 +59,20 @@ android { buildTypes { debug { applicationIdSuffix = '.debug' + buildConfigField "String", "SENTRY_DSN", '""' } release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release + buildConfigField "String", "SENTRY_DSN", "\"${System.getenv('SENTRY_DSN') ?: ''}\"" } nightly { initWith release applicationIdSuffix = '.nightly' signingConfig signingConfigs.release + buildConfigField "String", "SENTRY_DSN", "\"${System.getenv('SENTRY_DSN') ?: ''}\"" } } buildFeatures { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d2dcbbf308..814c8a6568 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1689,7 +1689,7 @@ - + diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt index 800bf9a553..e990e4e5a1 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt @@ -71,6 +71,10 @@ open class BaseApp : Application(), Configuration.Provider { super.onCreate() PlatformRegistry.applicationContext = this // TODO replace with OkHttp.initialize AppCompatDelegate.setDefaultNightMode(settings.theme) + // Initialize Sentry only if user has opted in + if (settings.isCrashAnalyticsEnabled) { + initializeSentry() + } // TLS 1.3 support for Android < 10 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { Security.insertProviderAt(Conscrypt.newProvider(), 1) @@ -101,4 +105,21 @@ open class BaseApp : Application(), Configuration.Provider { registerActivityLifecycleCallbacks(it) } } + + private fun initializeSentry() { + try { + io.sentry.android.core.SentryAndroid.init(this) { options -> + // DSN is read from BuildConfig which gets it from SENTRY_DSN environment variable + // Only set if DSN is provided (non-empty) + if (BuildConfig.SENTRY_DSN.isNotEmpty()) { + options.dsn = BuildConfig.SENTRY_DSN + options.isEnableAutoSessionTracking = true + options.environment = if (BuildConfig.DEBUG) "debug" else "production" + } + } + } catch (e: Exception) { + // Log error but don't crash if Sentry initialization fails + e.printStackTrace() + } + } } diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt index af6aa2ec37..ec676d8c8e 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt @@ -580,6 +580,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isStatsEnabled: Boolean get() = prefs.getBoolean(KEY_STATS_ENABLED, false) + var isCrashAnalyticsEnabled: Boolean + get() = prefs.getBoolean(KEY_CRASH_ANALYTICS_ENABLED, true) + set(value) = prefs.edit { putBoolean(KEY_CRASH_ANALYTICS_ENABLED, value) } + val isAutoLocalChaptersCleanupEnabled: Boolean get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false) @@ -817,6 +821,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_DISCORD_RPC = "discord_rpc" const val KEY_DISCORD_RPC_SKIP_NSFW = "discord_rpc_skip_nsfw" const val KEY_DISCORD_TOKEN = "discord_token" + const val KEY_CRASH_ANALYTICS_ENABLED = "crash_analytics_enabled" // keys for non-persistent preferences const val KEY_APP_VERSION = "app_version" diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt index 5ceeab3fb5..475b909230 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt @@ -79,7 +79,6 @@ import io.github.landwarderer.futon.search.ui.suggestion.SearchSuggestionViewMod import io.github.landwarderer.futon.search.ui.suggestion.adapter.SearchSuggestionAdapter import javax.inject.Inject import com.google.android.material.R as materialR -import io.sentry.Sentry @AndroidEntryPoint class MainActivity : BaseActivity(), AppBarOwner, BottomNavOwner, diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt index bfffce87c3..b3c544be74 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt @@ -16,6 +16,8 @@ import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import io.github.landwarderer.futon.R +import io.github.landwarderer.futon.core.prefs.AppSettings +import javax.inject.Inject import io.github.landwarderer.futon.core.model.titleResId import io.github.landwarderer.futon.core.nav.router import io.github.landwarderer.futon.core.ui.sheet.BaseAdaptiveSheet @@ -33,6 +35,9 @@ import java.util.Locale class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipClickListener, View.OnClickListener, ActivityResultCallback { + @Inject + lateinit var settings: AppSettings + private val viewModel by viewModels() private val backupSelectCall = registerForActivityResult( @@ -52,6 +57,10 @@ class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipC binding.chipBackup.setOnClickListener(this) binding.chipSync.setOnClickListener(this) binding.chipDirectories.setOnClickListener(this) + binding.switchCrashReporting.isChecked = settings.isCrashAnalyticsEnabled + binding.switchCrashReporting.setOnCheckedChangeListener { _, isChecked -> + settings.isCrashAnalyticsEnabled = isChecked + } viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged) viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged) @@ -125,4 +134,5 @@ class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipC }, ) } + } diff --git a/app/src/main/res/layout/sheet_welcome.xml b/app/src/main/res/layout/sheet_welcome.xml index 282979dd03..07a8a0509c 100644 --- a/app/src/main/res/layout/sheet_welcome.xml +++ b/app/src/main/res/layout/sheet_welcome.xml @@ -76,6 +76,44 @@ + + + + + + + + + + + + + + 16dp 8dp + 24dp 6dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bec5eb19d2..61055e288b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -906,4 +906,6 @@ Default directory for downloading manga This directory with all data will be deleted if you uninstall the application %1$s available + Crash Reporting + Send anonymous crash logs to help improve the app. Takes effect after restart. diff --git a/app/src/main/res/xml/pref_services.xml b/app/src/main/res/xml/pref_services.xml index f78877bb10..692b65fc79 100644 --- a/app/src/main/res/xml/pref_services.xml +++ b/app/src/main/res/xml/pref_services.xml @@ -77,4 +77,11 @@ app:allowDividerAbove="true" app:icon="@drawable/ic_discord" /> + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d44796dc0d..ed4a160a84 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ desugar = "2.1.5" diskLruCache = "1.5" documentfile = "1.1.0" fragment = "1.8.9" -gradle = "8.13.0" +gradle = "8.13.2" guava = "33.4.8-android" hilt = "1.3.0" json = "20250517"