diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt index cd5b17b5901c..6acb91e82614 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt @@ -22,14 +22,18 @@ import androidx.core.content.edit import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService -import com.ichi2.anki.CrashReportService.FEEDBACK_REPORT_ALWAYS -import com.ichi2.anki.CrashReportService.FEEDBACK_REPORT_ASK import com.ichi2.anki.R +import com.ichi2.anki.acraCoreConfigBuilder import com.ichi2.anki.analytics.UsageAnalytics +import com.ichi2.anki.common.crashreporting.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReporter +import com.ichi2.anki.common.crashreporting.CrashReporter.Companion.FEEDBACK_REPORT_ALWAYS +import com.ichi2.anki.common.crashreporting.CrashReporter.Companion.FEEDBACK_REPORT_ASK import com.ichi2.anki.logging.ProductionCrashReportingTree import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.servicelayer.ThrowableFilterService +import com.ichi2.anki.setDebugACRAConfig +import com.ichi2.anki.setProductionACRAConfig import com.ichi2.anki.testutil.GrantStoragePermission import org.acra.ACRA import org.acra.builder.ReportBuilder @@ -70,7 +74,7 @@ class ACRATest : InstrumentedTest() { @Throws(Exception::class) fun testDebugConfiguration() { // Debug mode overrides all saved state so no setup needed - CrashReportService.setDebugACRAConfig(sharedPrefs) + setDebugACRAConfig(sharedPrefs) assertArrayEquals( "Debug logcat arguments not set correctly", CrashReportService.acraCoreConfigBuilder @@ -90,9 +94,9 @@ class ACRATest : InstrumentedTest() { ) assertEquals( "ACRA feedback was not turned off correctly", - CrashReportService.FEEDBACK_REPORT_NEVER, + CrashReporter.FEEDBACK_REPORT_NEVER, sharedPrefs - .getString(CrashReportService.FEEDBACK_REPORT_KEY, "undefined"), + .getString(CrashReporter.FEEDBACK_REPORT_KEY, "undefined"), ) } @@ -100,11 +104,11 @@ class ACRATest : InstrumentedTest() { @Throws(Exception::class) fun testProductionConfigurationUserDisabled() { // set up as if the user had prefs saved to disable completely - setReportConfig(CrashReportService.FEEDBACK_REPORT_NEVER) + setReportConfig(CrashReporter.FEEDBACK_REPORT_NEVER) // ACRA initializes production logcat via annotation and we can't mock Build.DEBUG // That means we are restricted from verifying production logcat args and this is the debug case again - CrashReportService.setProductionACRAConfig(sharedPrefs) + setProductionACRAConfig(sharedPrefs) verifyDebugACRAPreferences() } @@ -115,7 +119,7 @@ class ACRATest : InstrumentedTest() { setReportConfig(FEEDBACK_REPORT_ASK) // If the user is set to ask, then it's production, with interaction mode dialog - CrashReportService.setProductionACRAConfig(sharedPrefs) + setProductionACRAConfig(sharedPrefs) verifyACRANotDisabled() assertToastMessage(R.string.feedback_for_manual_toast_text) @@ -134,7 +138,7 @@ class ACRATest : InstrumentedTest() { // If the user is set to always, then it's production, with interaction mode toast // will be useful with ACRA 5.2.0 - CrashReportService.setProductionACRAConfig(sharedPrefs) + setProductionACRAConfig(sharedPrefs) // The same class/method combo is only sent once, so we face a new method each time (should test that system later) val crash = Exception("testCrashReportSend at " + System.currentTimeMillis()) @@ -173,7 +177,7 @@ class ACRATest : InstrumentedTest() { ) // Now let's clear data - CrashReportService.deleteACRALimiterData(testContext) + CrashReportService.deleteLimiterData(testContext) // A third send should work again assertTrue( @@ -192,7 +196,7 @@ class ACRATest : InstrumentedTest() { setReportConfig(FEEDBACK_REPORT_ALWAYS) // If the user is set to always, then it's production, with interaction mode toast - CrashReportService.setProductionACRAConfig(sharedPrefs) + setProductionACRAConfig(sharedPrefs) verifyACRANotDisabled() assertToastMessage(R.string.feedback_auto_toast_text) @@ -207,7 +211,7 @@ class ACRATest : InstrumentedTest() { setReportConfig(FEEDBACK_REPORT_ALWAYS) // If the user is set to ask, then it's production, with interaction mode dialog - CrashReportService.setProductionACRAConfig(sharedPrefs) + setProductionACRAConfig(sharedPrefs) verifyACRANotDisabled() assertDialogEnabledStatus("dialog should be disabled when status is ALWAYS", false) @@ -226,7 +230,7 @@ class ACRATest : InstrumentedTest() { setReportConfig(FEEDBACK_REPORT_ASK) // If the user is set to ask, then it's production, with interaction mode dialog - CrashReportService.setProductionACRAConfig(sharedPrefs) + setProductionACRAConfig(sharedPrefs) verifyACRANotDisabled() assertToastMessage(R.string.feedback_for_manual_toast_text) @@ -282,7 +286,7 @@ class ACRATest : InstrumentedTest() { } private fun setAcraReportingMode(feedbackReportAlways: String) { - CrashReportService.setAcraReportingMode(feedbackReportAlways) + CrashReportService.setReportingMode(feedbackReportAlways) } @Throws(ACRAConfigurationException::class) @@ -333,7 +337,7 @@ class ACRATest : InstrumentedTest() { } private fun setReportConfig(feedbackReportAsk: String) { - sharedPrefs.edit { putString(CrashReportService.FEEDBACK_REPORT_KEY, feedbackReportAsk) } + sharedPrefs.edit { putString(CrashReporter.FEEDBACK_REPORT_KEY, feedbackReportAsk) } } private val sharedPrefs: SharedPreferences diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AcraCrashReporter.kt similarity index 86% rename from AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt rename to AnkiDroid/src/main/java/com/ichi2/anki/AcraCrashReporter.kt index d6e9cb66f248..d8aa1f045fb4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CrashReportService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AcraCrashReporter.kt @@ -26,6 +26,12 @@ import androidx.webkit.WebViewCompat import com.ichi2.anki.analytics.AnkiDroidCrashReportDialog import com.ichi2.anki.analytics.UsageAnalytics import com.ichi2.anki.analytics.UsageAnalytics.sendAnalyticsException +import com.ichi2.anki.common.crashreporting.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReporter +import com.ichi2.anki.common.crashreporting.CrashReporter.Companion.FEEDBACK_REPORT_ALWAYS +import com.ichi2.anki.common.crashreporting.CrashReporter.Companion.FEEDBACK_REPORT_ASK +import com.ichi2.anki.common.crashreporting.CrashReporter.Companion.FEEDBACK_REPORT_KEY +import com.ichi2.anki.common.crashreporting.CrashReporter.Companion.FEEDBACK_REPORT_NEVER import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.exception.ManuallyReportedException import com.ichi2.anki.exception.UserSubmittedException @@ -43,13 +49,7 @@ import org.acra.config.ToastConfigurationBuilder import org.acra.sender.HttpSender import timber.log.Timber -object CrashReportService { - // ACRA constants used for stored preferences - const val FEEDBACK_REPORT_KEY = "reportErrorMode" - const val FEEDBACK_REPORT_ASK = "2" - const val FEEDBACK_REPORT_NEVER = "1" - const val FEEDBACK_REPORT_ALWAYS = "0" - +private object AcraCrashReporter : CrashReporter { /** Our ACRA configurations, initialized during Application.onCreate() */ @JvmStatic private var logcatArgs = @@ -166,7 +166,7 @@ object CrashReportService { */ @JvmStatic fun initialize(application: Application) { - CrashReportService.application = application + this.application = application // FIXME ACRA needs to reinitialize after language is changed, but with the new language // this is difficult because the Application (AnkiDroidApp) does not change it's baseContext // perhaps a solution could be to change AnkiDroidApp to have a context wrapper that it sets @@ -174,7 +174,7 @@ object CrashReportService { // in GeneralSettingsFragment for the language dialog change listener, the context wrapper // could be updated directly with the new locale code so that calling getString on would fetch // the new language string ? - toastText = ToastType.AUTO_TOAST.getToastMessage(CrashReportService.application) + toastText = ToastType.AUTO_TOAST.getToastMessage(application) // Setup logging and crash reporting if (BuildConfig.DEBUG) { @@ -195,7 +195,7 @@ object CrashReportService { * Set the reporting mode for ACRA based on the value of the FEEDBACK_REPORT_KEY preference * @param value value of FEEDBACK_REPORT_KEY preference */ - fun setAcraReportingMode(value: String) { + override fun setReportingMode(value: String) { application.sharedPrefs().edit { // Set the ACRA disable value if (value == FEEDBACK_REPORT_NEVER) { @@ -225,7 +225,7 @@ object CrashReportService { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun setDebugACRAConfig(prefs: SharedPreferences) { // Disable crash reporting - setAcraReportingMode(FEEDBACK_REPORT_NEVER) + setReportingMode(FEEDBACK_REPORT_NEVER) prefs.edit { putString(FEEDBACK_REPORT_KEY, FEEDBACK_REPORT_NEVER) } // Use a wider logcat filter in case crash reporting manually re-enabled logcatArgs = arrayOf("-t", "1500", "-v", "long", "ACRA:S") @@ -240,7 +240,7 @@ object CrashReportService { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun setProductionACRAConfig(prefs: SharedPreferences) { // Enable or disable crash reporting based on user setting - setAcraReportingMode(prefs.getString(FEEDBACK_REPORT_KEY, FEEDBACK_REPORT_ASK)!!) + setReportingMode(prefs.getString(FEEDBACK_REPORT_KEY, FEEDBACK_REPORT_ASK)!!) } private fun fetchWebViewInformation(): HashMap { @@ -262,17 +262,24 @@ object CrashReportService { } /** Used when we don't have an exception to throw, but we know something is wrong and want to diagnose it */ - fun sendExceptionReport( + override fun sendExceptionReport( message: String?, origin: String?, ) = sendExceptionReport(ManuallyReportedException(message), origin) - fun sendExceptionReport( + override fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String?, + onlyIfSilent: Boolean, + ) = sendExceptionReport(e, origin, additionalInfo, onlyIfSilent, application.applicationContext) + + override fun sendExceptionReport( e: Throwable, origin: String?, - additionalInfo: String? = null, - onlyIfSilent: Boolean = false, - context: Context = application.applicationContext, + additionalInfo: String?, + onlyIfSilent: Boolean, + context: Context, ) { sendAnalyticsException(e, false) AnkiDroidApp.sentExceptionReportHack = true @@ -297,7 +304,7 @@ object CrashReportService { fun isProperServiceProcess(): Boolean = ACRA.isACRASenderServiceProcess() - fun isAcraEnabled( + override fun isEnabled( context: Context, defaultValue: Boolean, ): Boolean { @@ -314,7 +321,7 @@ object CrashReportService { * * @param context the context leading to the directory with ACRA limiter data */ - fun deleteACRALimiterData(context: Context) { + override fun deleteLimiterData(context: Context) { try { LimiterData().store(context) } catch (e: Exception) { @@ -322,13 +329,13 @@ object CrashReportService { } } - fun onPreferenceChanged( + override fun onPreferenceChanged( ctx: Context, newValue: String, ) { - setAcraReportingMode(newValue) + setReportingMode(newValue) // If the user changed error reporting, make sure future reports have a chance to post - deleteACRALimiterData(ctx) + deleteLimiterData(ctx) // We also need to re-chain our UncaughtExceptionHandlers UsageAnalytics.reInitialize() ThrowableFilterService.reInitialize() @@ -338,7 +345,8 @@ object CrashReportService { * @return the status of the report, true if the report was sent, false if the report is already * submitted */ - fun sendReport(ankiActivity: AnkiActivity): Boolean { + override fun sendReport(activity: android.app.Activity): Boolean { + val ankiActivity = activity as AnkiActivity val preferences = ankiActivity.sharedPrefs() val reportMode = preferences.getString(FEEDBACK_REPORT_KEY, "") return if (FEEDBACK_REPORT_NEVER == reportMode) { @@ -359,7 +367,7 @@ object CrashReportService { val currentTimestamp = TimeManager.time.intTimeMS() val lastReportTimestamp = getTimestampOfLastReport(activity) return if (currentTimestamp - lastReportTimestamp > MIN_INTERVAL_MS) { - deleteACRALimiterData(activity) + deleteLimiterData(activity) sendExceptionReport( UserSubmittedException(EXCEPTION_MESSAGE), "AnkiDroidApp.HelpDialog", @@ -385,35 +393,23 @@ object CrashReportService { } /** - * Runs the provided block, catching [Exception], logging it and reporting it to [CrashReportService] - * - * **Example** - * ``` - * runCatchingWithReport("callingMethod", onlyIfSilent = true) { - * doSomethingRisky() - * } - * ``` - * - * **Note**: This differs from [runCatching] - `Error` is thrown - * - * @param origin Data logged to Timber, and provided as the 'origin' field in the error report - * @param onlyIfSilent Skip crash report if the crash reporting service is not 'always accept' - * @param block Code to execute - * - * @throws Error If raised, this will be reported and rethrown - * - * @return A Result containing either the successful result of [block] or the [Exception] thrown + * Initializes ACRA crash reporting and wires it up as the + * global [CrashReportService] reporter. */ -fun runCatchingWithReport( - origin: String?, - onlyIfSilent: Boolean = false, - block: () -> T, -): Result = - try { - Result.success(block()) - } catch (e: Throwable) { - Timber.w(e, origin) - CrashReportService.sendExceptionReport(e, origin, onlyIfSilent = onlyIfSilent) - if (e is Error) throw e - Result.failure(e) - } +context(application: Application) +fun initializeAcraCrashReporter() { + AcraCrashReporter.initialize(application) + CrashReportService.setReporter(AcraCrashReporter) +} + +fun isAcraSenderProcess(): Boolean = AcraCrashReporter.isProperServiceProcess() + +@VisibleForTesting(otherwise = VisibleForTesting.NONE) +val CrashReportService.acraCoreConfigBuilder: CoreConfigurationBuilder + get() = AcraCrashReporter.acraCoreConfigBuilder + +@VisibleForTesting(otherwise = VisibleForTesting.NONE) +fun setDebugACRAConfig(sharedPrefs: SharedPreferences) = AcraCrashReporter.setDebugACRAConfig(sharedPrefs) + +@VisibleForTesting(otherwise = VisibleForTesting.NONE) +fun setProductionACRAConfig(sharedPrefs: SharedPreferences) = AcraCrashReporter.setProductionACRAConfig(sharedPrefs) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt index 43fca6a427e9..6e8d3e195daa 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -62,6 +62,7 @@ import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.ShortcutGroupProvider import com.ichi2.anki.android.input.shortcut import com.ichi2.anki.common.annotations.LegacyNotifications +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.utils.annotation.KotlinCleanup import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.compat.CompatHelper.Companion.registerReceiverCompat diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt index d91e85ca3f08..6f03e667c082 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt @@ -35,11 +35,11 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.MutableLiveData import anki.collection.OpChanges import com.ichi2.anki.AnkiDroidApp.Companion.sharedPreferencesTestingOverride -import com.ichi2.anki.CrashReportService.sendExceptionReport import com.ichi2.anki.analytics.UsageAnalytics import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository import com.ichi2.anki.common.annotations.LegacyNotifications import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService.sendExceptionReport import com.ichi2.anki.common.utils.annotation.KotlinCleanup import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.contextmenu.AnkiCardContextMenu @@ -136,7 +136,7 @@ open class AnkiDroidApp : // Ensures any change is propagated to widgets ChangeManager.subscribe(this) - CrashReportService.initialize(this) + initializeAcraCrashReporter() val logType = LogType.value when (logType) { LogType.DEBUG -> Timber.plant(DebugTree()) @@ -167,7 +167,7 @@ open class AnkiDroidApp : } // Stop after analytics and logging are initialised. - if (CrashReportService.isProperServiceProcess()) { + if (isAcraSenderProcess()) { Timber.d("Skipping AnkiDroidApp.onCreate from ACRA sender process") return } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index ec3c06612208..979ecd5a38ec 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt @@ -42,6 +42,7 @@ import com.ichi2.anki.CrashReportData.HelpAction.AnkiBackendLink import com.ichi2.anki.CrashReportData.HelpAction.OpenDeckOptions import com.ichi2.anki.android.AnkiBroadcastReceiver import com.ichi2.anki.common.annotations.UseContextParameter +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.dialogs.DatabaseErrorDialog import com.ichi2.anki.dialogs.DatabaseErrorDialog.DatabaseErrorDialogType import com.ichi2.anki.exception.StorageAccessException diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt index b0a6be526db4..ac047442b381 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DayRolloverHandler.kt @@ -35,6 +35,7 @@ import anki.collection.OpChanges import anki.collection.opChanges import com.ichi2.anki.CollectionManager.withOpenColOrNull import com.ichi2.anki.android.AnkiBroadcastReceiver +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.exception.ManuallyReportedException import com.ichi2.anki.libanki.EpochSeconds import com.ichi2.anki.libanki.sched.Scheduler diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 43e573d94f18..da809c281452 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -93,6 +93,7 @@ import com.ichi2.anki.android.back.exitViaDoubleTapBackCallback import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.shortcut import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.common.utils.annotation.KotlinCleanup import com.ichi2.anki.compat.CompatHelper.Companion.getSerializableCompat diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt index c05484e451f4..1042fc7312b3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt @@ -27,6 +27,7 @@ import android.os.Parcelable import androidx.annotation.CheckResult import androidx.annotation.RequiresApi import androidx.core.content.edit +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.compat.CompatHelper.Companion.sdkVersion import com.ichi2.anki.dialogs.DatabaseErrorDialog import com.ichi2.anki.exception.StorageAccessException diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt index 254d9e067adf..256da728b3a9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/MediaRegistration.kt @@ -22,6 +22,7 @@ import android.graphics.BitmapFactory import android.net.Uri import androidx.annotation.CheckResult import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.libanki.exception.EmptyMediaException import com.ichi2.anki.multimediacard.fields.ImageField diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt index d968c1df14e2..6a9817e6beec 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt @@ -90,6 +90,7 @@ import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.ShortcutGroupProvider import com.ichi2.anki.android.input.shortcut import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.utils.annotation.KotlinCleanup import com.ichi2.anki.common.utils.ext.ifZero import com.ichi2.anki.compat.CompatHelper.Companion.getSerializableCompat diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 24d14780d2ca..6fafd27f5b9d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -69,6 +69,7 @@ import com.ichi2.anki.Whiteboard.OnPaintColorChangeListener import com.ichi2.anki.cardviewer.Gesture import com.ichi2.anki.cardviewer.ViewerCommand import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.libanki.Card import com.ichi2.anki.libanki.CardId diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt index fa13bc77b434..610f8ed08799 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt @@ -38,6 +38,7 @@ import androidx.fragment.app.Fragment import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.SharedDecksActivity.Companion.DOWNLOAD_FILE import com.ichi2.anki.android.AnkiBroadcastReceiver +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.anki.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.anki.databinding.FragmentSharedDecksDownloadBinding diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt index 444812e83b66..14fa6d89a753 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt @@ -42,6 +42,7 @@ import anki.collection.OpChanges import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.backend.stripHTMLScriptAndStyleTags +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog import com.ichi2.anki.filtered.FilteredDeckOptionsFragment import com.ichi2.anki.libanki.Collection diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/analytics/AnkiDroidCrashReportDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/analytics/AnkiDroidCrashReportDialog.kt index d8b2a806c923..6680ad656765 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/analytics/AnkiDroidCrashReportDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/analytics/AnkiDroidCrashReportDialog.kt @@ -22,8 +22,9 @@ import android.content.DialogInterface import android.os.Bundle import android.view.View import androidx.core.content.edit -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReporter import com.ichi2.anki.databinding.DialogFeedbackBinding import com.ichi2.anki.preferences.sharedPrefs import org.acra.dialog.CrashReportDialog @@ -87,11 +88,11 @@ class AnkiDroidCrashReportDialog : if (autoReport) { preferences.edit { putString( - CrashReportService.FEEDBACK_REPORT_KEY, - CrashReportService.FEEDBACK_REPORT_ALWAYS, + CrashReporter.FEEDBACK_REPORT_KEY, + CrashReporter.FEEDBACK_REPORT_ALWAYS, ) } - CrashReportService.setAcraReportingMode(CrashReportService.FEEDBACK_REPORT_ALWAYS) + CrashReportService.setReportingMode(CrashReporter.FEEDBACK_REPORT_ALWAYS) } // Send the crash report helper!!.sendCrash(binding.userComment.text.toString(), "") @@ -100,7 +101,7 @@ class AnkiDroidCrashReportDialog : // The limiter persists it's limit info *before* the user cancels. // Therefore, on cancel, purge limits to make sure the user may actually send in future. // Better to maybe send to many reports than definitely too few. - CrashReportService.deleteACRALimiterData(this) + CrashReportService.deleteLimiterData(this) helper!!.cancelReports() } finish() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/backend/BackendDBUtils.kt b/AnkiDroid/src/main/java/com/ichi2/anki/backend/BackendDBUtils.kt index ed8d3befe2bd..89c99db74b41 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/backend/BackendDBUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/backend/BackendDBUtils.kt @@ -20,7 +20,7 @@ import android.content.Context import androidx.annotation.CheckResult import androidx.sqlite.db.SupportSQLiteDatabase import com.ichi2.anki.CollectionManager -import com.ichi2.anki.CrashReportService.sendExceptionReport +import com.ichi2.anki.common.crashreporting.CrashReportService.sendExceptionReport import com.ichi2.anki.dialogs.DatabaseErrorDialog import com.ichi2.anki.libanki.DB import net.ankiweb.rsdroid.Backend diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt index fb3de3c95d31..bd7fa673ec39 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt @@ -38,7 +38,6 @@ import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.CollectionManager import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.CrashReportService import com.ichi2.anki.Flag import com.ichi2.anki.PreviewerDestination import com.ichi2.anki.browser.CardBrowserViewModel.ChangeMultiSelectMode.MultiSelectCause @@ -54,6 +53,7 @@ import com.ichi2.anki.browser.search.SearchFilters import com.ichi2.anki.browser.search.SearchRequest import com.ichi2.anki.browser.search.SearchString import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.utils.ext.indexOfOrNull import com.ichi2.anki.export.ExportDialogFragment.ExportType import com.ichi2.anki.launchCatchingIO diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/BackupPromptDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/BackupPromptDialog.kt index 9304ba04f8d3..ff50611f3ed0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/BackupPromptDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/BackupPromptDialog.kt @@ -22,9 +22,9 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.CrashReportService import com.ichi2.anki.DeckPicker import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.compat.CompatHelper.Companion.getPackageInfoCompat import com.ichi2.anki.compat.PackageInfoFlagsCompat diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/TtsPlaybackErrorDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/TtsPlaybackErrorDialog.kt index 160ec0f5c4c3..00146fa4c9a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/TtsPlaybackErrorDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/TtsPlaybackErrorDialog.kt @@ -20,9 +20,9 @@ import android.app.Activity import android.content.Intent import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentManager -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.TtsVoices +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.libanki.TTSTag import com.ichi2.anki.utils.openUrl import com.ichi2.utils.show diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/help/HelpItemActionsDispatcher.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/help/HelpItemActionsDispatcher.kt index 79cc60dac60c..100a0c9e0ea4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/help/HelpItemActionsDispatcher.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/help/HelpItemActionsDispatcher.kt @@ -16,8 +16,8 @@ package com.ichi2.anki.dialogs.help import androidx.annotation.StringRes import com.ichi2.anki.AnkiActivity import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.utils.AdaptionUtil import com.ichi2.utils.IntentUtil diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioRecordingFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioRecordingFragment.kt index a71e430903a1..3c5f1d7c92ae 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioRecordingFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioRecordingFragment.kt @@ -25,9 +25,9 @@ import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.databinding.FragmentAudioRecordingBinding import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT_FIELD_INDEX diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioVideoFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioVideoFragment.kt index d944319197aa..5eafb517c3e1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioVideoFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioVideoFragment.kt @@ -34,9 +34,9 @@ import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.anki.databinding.FragmentAudioVideoBinding diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaFragment.kt index 88d270385e06..213878dc6604 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaFragment.kt @@ -35,9 +35,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.ichi2.anki.CollectionManager.TR -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.anki.dialogs.DiscardChangesDialog import com.ichi2.anki.multimediacard.IMultimediaEditableNote diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaUtils.kt b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaUtils.kt index e8d909b13d8e..cf8eb7b67764 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaUtils.kt @@ -26,7 +26,7 @@ import android.provider.DocumentsContract import android.provider.MediaStore import androidx.core.content.ContentResolverCompat import androidx.core.net.toUri -import com.ichi2.anki.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.common.time.getTimestamp import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/ProfileManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/ProfileManager.kt index 61db266ba782..7ab079fc0773 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/ProfileManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/ProfileManager.kt @@ -26,8 +26,8 @@ import android.webkit.WebView import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.core.content.edit -import com.ichi2.anki.CrashReportService import com.ichi2.anki.IntentHandler +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.common.time.getTimestamp import org.acra.ACRA diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt index bb4e70ab9520..ee7ff7faa5a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt @@ -35,8 +35,8 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.ichi2.anki.AnkiActivity import com.ichi2.anki.CollectionManager.TR -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.databinding.ActivityManageNoteTypesBinding import com.ichi2.anki.dialogs.dismissLoadingDialog import com.ichi2.anki.dialogs.showLoadingDialog diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/observability/ChangeManager.kt b/AnkiDroid/src/main/java/com/ichi2/anki/observability/ChangeManager.kt index e5def5d6db96..9ce35bdc2338 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/observability/ChangeManager.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/observability/ChangeManager.kt @@ -38,7 +38,7 @@ import anki.collection.OpChangesWithCount import anki.collection.OpChangesWithId import anki.collection.opChanges import anki.import_export.ImportResponse -import com.ichi2.anki.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.utils.ext.ifNotZero import org.jetbrains.annotations.Contract import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt index d1f4b4466ada..2cb1414c153c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt @@ -31,11 +31,11 @@ import anki.collection.Progress import com.google.android.material.appbar.MaterialToolbar import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.CrashReportService import com.ichi2.anki.ProgressContext import com.ichi2.anki.R import com.ichi2.anki.SingleFragmentActivity import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.libanki.updateDeckConfigsRaw diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageChromeClient.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageChromeClient.kt index 68e1056f4ecd..ff4f6d3ca11d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageChromeClient.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageChromeClient.kt @@ -20,8 +20,8 @@ import android.webkit.JsResult import android.webkit.WebChromeClient import android.webkit.WebView import androidx.appcompat.app.AlertDialog -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.utils.cancelable import com.ichi2.utils.message import com.ichi2.utils.negativeButton diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/DeveloperOptionsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/DeveloperOptionsFragment.kt index 0a4f16c70bfb..60770251d342 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/DeveloperOptionsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/DeveloperOptionsFragment.kt @@ -24,9 +24,9 @@ import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.BuildConfig import com.ichi2.anki.CollectionHelper import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.analytics.UsageAnalytics +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.dialogs.TtsVoicesDialogFragment import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.settings.Prefs @@ -60,7 +60,7 @@ class DeveloperOptionsFragment : SettingsFragment() { requirePreference(R.string.pref_trigger_crash_key).setOnPreferenceClickListener { // If we don't delete the limiter data, our test crash may not go through, // but we are triggering it very much on purpose, we want to see the crash in ACRA - this.context?.let { c -> CrashReportService.deleteACRALimiterData(c) } + this.context?.let { c -> CrashReportService.deleteLimiterData(c) } Timber.w("Crash triggered on purpose from advanced preferences in debug mode") throw RuntimeException("This is a test crash") diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/GeneralSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/GeneralSettingsFragment.kt index de4f79409b1d..d38543cf6108 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/GeneralSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/GeneralSettingsFragment.kt @@ -22,8 +22,8 @@ import androidx.preference.SwitchPreferenceCompat import anki.config.ConfigKey import com.ichi2.anki.CollectionManager import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.contextmenu.AnkiCardContextMenu import com.ichi2.anki.contextmenu.CardBrowserContextMenu import com.ichi2.anki.launchCatchingTask diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt index ce67ce319109..ddb22810dc96 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/SyncSettingsFragment.kt @@ -23,8 +23,8 @@ import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.R import com.ichi2.anki.account.AccountActivity +import com.ichi2.anki.common.crashreporting.runCatchingWithReport import com.ichi2.anki.launchCatchingTask -import com.ichi2.anki.runCatchingWithReport import com.ichi2.anki.settings.Prefs import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.ext.ifNullOrEmpty diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt index d2b09b7c01ee..5b825900bc65 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt @@ -34,8 +34,8 @@ import anki.scheduler.CardAnswer import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.BuildConfig import com.ichi2.anki.CollectionManager -import com.ichi2.anki.CrashReportService import com.ichi2.anki.FlashCardsContract +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.common.utils.annotation.KotlinCleanup import com.ichi2.anki.libanki.Card diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt index 869238328686..3f4baceb8b8f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt @@ -21,7 +21,7 @@ import android.content.SharedPreferences import androidx.annotation.VisibleForTesting import androidx.core.content.edit import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.settings.Prefs import com.ichi2.anki.showError diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/DebugInfoService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/DebugInfoService.kt index a58416247cb5..259fae3be650 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/DebugInfoService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/DebugInfoService.kt @@ -20,7 +20,7 @@ import android.content.Context import android.os.Build import com.ichi2.anki.BuildConfig import com.ichi2.anki.CollectionManager -import com.ichi2.anki.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.utils.VersionUtils.pkgVersionName import com.ichi2.utils.getWebViewInfo import org.acra.util.Installation @@ -53,7 +53,7 @@ object DebugInfoService { .replace("\n", " \n") } - private fun isSendingCrashReports(context: Context): Boolean = CrashReportService.isAcraEnabled(context, false) + private fun isSendingCrashReports(context: Context): Boolean = CrashReportService.isEnabled(context, false) } /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt b/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt index 97f767f3d5cf..06fba5c68ecd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt @@ -27,8 +27,8 @@ import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.onAttachedToWindow2 import com.ichi2.anki.BuildConfig -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.exception.ManuallyReportedException import com.ichi2.anki.showThemedToast import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/utils/Resources.kt b/AnkiDroid/src/main/java/com/ichi2/anki/utils/Resources.kt index 532e5f75963a..79d56d54834b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/utils/Resources.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/utils/Resources.kt @@ -21,8 +21,8 @@ import android.content.res.Resources import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.fragment.app.FragmentActivity -import com.ichi2.anki.CrashReportService import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService /** * @param resId must be a [StringRes] or a [PluralsRes] diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/utils/ShortcutUtils.kt b/AnkiDroid/src/main/java/com/ichi2/anki/utils/ShortcutUtils.kt index 2d150e7fba78..370fc04a765f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/utils/ShortcutUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/utils/ShortcutUtils.kt @@ -18,7 +18,7 @@ package com.ichi2.anki.utils import android.content.Context import androidx.core.content.pm.ShortcutManagerCompat -import com.ichi2.anki.runCatchingWithReport +import com.ichi2.anki.common.crashreporting.runCatchingWithReport import timber.log.Timber /** diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/AppLoadedFromBackupWorkaround.kt b/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/AppLoadedFromBackupWorkaround.kt index 8f5c205eeefd..ce3c4577e927 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/AppLoadedFromBackupWorkaround.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/AppLoadedFromBackupWorkaround.kt @@ -20,8 +20,8 @@ import android.app.Activity import android.os.Bundle import android.os.Process import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.exception.ManuallyReportedException import com.ichi2.anki.showThemedToast import com.ichi2.themes.Themes diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/SafeWebViewLayout.kt b/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/SafeWebViewLayout.kt index 6a24a17e3d1f..f9633d63f8de 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/SafeWebViewLayout.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/workarounds/SafeWebViewLayout.kt @@ -27,7 +27,7 @@ import androidx.annotation.MainThread import androidx.fragment.app.Fragment import androidx.fragment.app.findFragment import com.ichi2.anki.BuildConfig -import com.ichi2.anki.runCatchingWithReport +import com.ichi2.anki.common.crashreporting.runCatchingWithReport import timber.log.Timber open class SafeWebViewLayout : diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabActivityHelper.kt b/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabActivityHelper.kt index ff3ee7b627d1..79cb571c3095 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabActivityHelper.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabActivityHelper.kt @@ -24,8 +24,8 @@ import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.snackbar.showSnackbar import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsFallback.kt b/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsFallback.kt index a1c15f8cf5f5..ee8bc4027adb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsFallback.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsFallback.kt @@ -17,8 +17,8 @@ package com.ichi2.compat.customtabs import android.app.Activity import android.content.Intent import android.net.Uri -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.showThemedToast import com.ichi2.compat.customtabs.CustomTabActivityHelper.CustomTabFallback diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsHelper.kt b/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsHelper.kt index b01aaa86c356..6ca320583338 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsHelper.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/customtabs/CustomTabsHelper.kt @@ -19,12 +19,12 @@ import android.content.Intent import android.content.pm.PackageManager import androidx.browser.customtabs.CustomTabsService import androidx.core.net.toUri +import com.ichi2.anki.common.crashreporting.runCatchingWithReport import com.ichi2.anki.compat.CompatHelper.Companion.queryIntentActivitiesCompat import com.ichi2.anki.compat.CompatHelper.Companion.resolveActivityCompat import com.ichi2.anki.compat.CompatHelper.Companion.resolveServiceCompat import com.ichi2.anki.compat.GET_RESOLVED_FILTER_L import com.ichi2.anki.compat.ResolveInfoFlagsCompat -import com.ichi2.anki.runCatchingWithReport import timber.log.Timber /** diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/BitmapUtil.kt b/AnkiDroid/src/main/java/com/ichi2/utils/BitmapUtil.kt index 9cba3846ec48..d8fe08618eef 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/BitmapUtil.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/BitmapUtil.kt @@ -24,7 +24,7 @@ import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.widget.ImageView import androidx.annotation.CheckResult -import com.ichi2.anki.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReportService import timber.log.Timber import java.io.File import java.io.FileInputStream diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ExceptionUtil.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ExceptionUtil.kt index 164236b28c71..058c04b8bcbf 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ExceptionUtil.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ExceptionUtil.kt @@ -17,8 +17,8 @@ package com.ichi2.utils import android.content.Context import androidx.annotation.CheckResult -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.showThemedToast import java.io.PrintWriter import java.io.StringWriter diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt index bf6732cad5fb..fb607f56b6db 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/ImportUtils.kt @@ -29,9 +29,9 @@ import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import com.ichi2.anki.AnkiActivity import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.dialogs.DialogHandler diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/VersionUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/VersionUtils.kt index d1e43e83519d..619d8dd014da 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/VersionUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/VersionUtils.kt @@ -20,7 +20,7 @@ import android.content.Context import android.content.pm.PackageManager import androidx.core.content.pm.PackageInfoCompat import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.compat.CompatHelper.Companion.getPackageInfoCompat import com.ichi2.anki.compat.PackageInfoFlagsCompat import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt index caa565323734..4ce9af9d87e2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt @@ -28,8 +28,8 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.pm.PackageInfoCompat import androidx.webkit.WebViewCompat import com.ichi2.anki.AnkiActivity -import com.ichi2.anki.CrashReportService import com.ichi2.anki.R +import com.ichi2.anki.common.crashreporting.CrashReportService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/cardanalysis/CardAnalysisWidget.kt b/AnkiDroid/src/main/java/com/ichi2/widget/cardanalysis/CardAnalysisWidget.kt index f678b357d09e..e7606b3163b7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/cardanalysis/CardAnalysisWidget.kt +++ b/AnkiDroid/src/main/java/com/ichi2/widget/cardanalysis/CardAnalysisWidget.kt @@ -25,10 +25,10 @@ import android.content.Intent import android.view.View import android.widget.RemoteViews import com.ichi2.anki.AnkiDroidApp -import com.ichi2.anki.CrashReportService import com.ichi2.anki.IntentHandler.Companion.intentToReviewDeckFromShortcuts import com.ichi2.anki.R import com.ichi2.anki.analytics.UsageAnalytics +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.isCollectionEmpty import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.libanki.Decks.Companion.NOT_FOUND_DECK_ID diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/deckpicker/DeckPickerWidget.kt b/AnkiDroid/src/main/java/com/ichi2/widget/deckpicker/DeckPickerWidget.kt index a10a54f150b3..a42d1595867d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/deckpicker/DeckPickerWidget.kt +++ b/AnkiDroid/src/main/java/com/ichi2/widget/deckpicker/DeckPickerWidget.kt @@ -26,10 +26,10 @@ import android.view.View import android.widget.RemoteViews import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.CrashReportService import com.ichi2.anki.IntentHandler.Companion.intentToReviewDeckFromShortcuts import com.ichi2.anki.R import com.ichi2.anki.analytics.UsageAnalytics +import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.isCollectionEmpty import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.pages.DeckOptionsDestination diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt index fe701ed0597b..19868f25fc14 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/AnkiDroidAppTest.kt @@ -16,7 +16,7 @@ package com.ichi2.anki import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.ichi2.anki.CrashReportService.sendExceptionReport +import com.ichi2.anki.common.crashreporting.CrashReportService.sendExceptionReport import org.junit.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.runner.RunWith diff --git a/common/android/src/main/java/com/ichi2/anki/common/crashreporting/CrashReportService.kt b/common/android/src/main/java/com/ichi2/anki/common/crashreporting/CrashReportService.kt new file mode 100644 index 000000000000..7d427fdd5f2f --- /dev/null +++ b/common/android/src/main/java/com/ichi2/anki/common/crashreporting/CrashReportService.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022 lukstbit + * Copyright (c) 2026 Ashish Yadav + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.common.crashreporting + +import android.app.Activity +import android.content.Context +import androidx.annotation.VisibleForTesting +import timber.log.Timber + +/** + * Interface for crash/exception reporting. + * + * Implemented in the app module by the ACRA-backed crash reporter. + * + * TODO: Remove Context/Activity parameters from this interface and use the + * application context set during initialization instead. Context is only used + * for SharedPreferences and LimiterData, so an Application context is likely + * sufficient. Removing them enables use from java-library modules. + * Note: profile-aware contexts may affect this decision. + */ +interface CrashReporter { + fun sendExceptionReport( + message: String?, + origin: String?, + ) + + fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String? = null, + onlyIfSilent: Boolean = false, + ) + + fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String? = null, + onlyIfSilent: Boolean = false, + context: Context, + ) + + fun onPreferenceChanged( + ctx: Context, + newValue: String, + ) + + fun deleteLimiterData(context: Context) + + fun setReportingMode(value: String) + + fun isEnabled( + context: Context, + defaultValue: Boolean, + ): Boolean + + fun sendReport(activity: Activity): Boolean + + companion object { + const val FEEDBACK_REPORT_KEY = "reportErrorMode" + const val FEEDBACK_REPORT_ASK = "2" + const val FEEDBACK_REPORT_NEVER = "1" + const val FEEDBACK_REPORT_ALWAYS = "0" + } +} + +/** + * Global crash reporting service. Delegates to the [CrashReporter] implementation + * set during app initialization. + * + * Usage: + * ``` + * CrashReportService.sendExceptionReport(exception, "MyClass.myMethod") + * ``` + */ +object CrashReportService { + lateinit var instance: CrashReporter + private set + + fun setReporter(reporter: CrashReporter) { + instance = reporter + } + + @VisibleForTesting + fun getReporter(): CrashReporter = instance + + /** + * Reports a non-fatal issue without a [Throwable]. + * @param message Description of what went wrong. + * @param origin The class or method name where the issue occurred. + */ + fun sendExceptionReport( + message: String?, + origin: String?, + ) = instance.sendExceptionReport(message, origin) + + // TODO: remove these from instance and handle the conversion + // of parameters in this class/an extension method + + /** + * Reports a caught [Throwable] to the crash reporting service. + * @param e The exception or error to report. + * @param origin The tag or location where it occurred. + * @param additionalInfo Optional extra metadata to include in the report. + * @param onlyIfSilent If true, only sends the report if the user has opted into automatic reporting. + */ + fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String? = null, + onlyIfSilent: Boolean = false, + ) = instance.sendExceptionReport(e, origin, additionalInfo, onlyIfSilent) + + /** + * Reports a caught [Throwable] with a specific [Context]. + * Use this when the application context is insufficient + */ + fun sendExceptionReport( + e: Throwable, + origin: String?, + additionalInfo: String? = null, + onlyIfSilent: Boolean = false, + context: Context, + ) = instance.sendExceptionReport(e, origin, additionalInfo, onlyIfSilent, context) + + /** + * Manually triggers a crash report, often used for user-initiated feedback. + * @return True if the report was successfully queued; false if rate-limited. + */ + fun sendReport(activity: Activity): Boolean = instance.sendReport(activity) + + fun onPreferenceChanged( + ctx: Context, + newValue: String, + ) = instance.onPreferenceChanged(ctx, newValue) + + fun deleteLimiterData(context: Context) = instance.deleteLimiterData(context) + + fun setReportingMode(value: String) = instance.setReportingMode(value) + + fun isEnabled( + context: Context, + defaultValue: Boolean, + ): Boolean = instance.isEnabled(context, defaultValue) +} + +/** + * Runs the provided block, catching [Exception], logging it and reporting it to [CrashReportService] + * + * **Example** + * ``` + * runCatchingWithReport("callingMethod", onlyIfSilent = true) { + * doSomethingRisky() + * } + * ``` + * + * **Note**: This differs from [runCatching] - `Error` is thrown + * + * @param origin Data logged to Timber, and provided as the 'origin' field in the error report + * @param onlyIfSilent Skip crash report if the crash reporting service is not 'always accept' + * @param block Code to execute + * + * @throws Error If raised, this will be reported and rethrown + * + * @return A Result containing either the successful result of [block] or the [Exception] thrown + */ +fun runCatchingWithReport( + origin: String?, + onlyIfSilent: Boolean = false, + block: () -> T, +): Result = + try { + Result.success(block()) + } catch (e: Throwable) { + Timber.w(e, origin) + CrashReportService.sendExceptionReport(e, origin, onlyIfSilent = onlyIfSilent) + if (e is Error) throw e + Result.failure(e) + }