Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ACRATest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -90,21 +94,21 @@ 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"),
)
}

@Test
@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()
}

Expand All @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -282,7 +286,7 @@ class ACRATest : InstrumentedTest() {
}

private fun setAcraReportingMode(feedbackReportAlways: String) {
CrashReportService.setAcraReportingMode(feedbackReportAlways)
CrashReportService.setReportingMode(feedbackReportAlways)
}

@Throws(ACRAConfigurationException::class)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down Expand Up @@ -166,15 +166,15 @@ 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
// as baseContext, and that wrapper allows a resources/configuration update, then
// 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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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")
Expand All @@ -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<String, String> {
Expand All @@ -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
Expand All @@ -297,7 +304,7 @@ object CrashReportService {

fun isProperServiceProcess(): Boolean = ACRA.isACRASenderServiceProcess()

fun isAcraEnabled(
override fun isEnabled(
context: Context,
defaultValue: Boolean,
): Boolean {
Expand All @@ -314,21 +321,21 @@ 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) {
Timber.w(e, "Unable to clear ACRA limiter data")
}
}

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()
Expand All @@ -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) {
Expand All @@ -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",
Expand All @@ -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 <T> runCatchingWithReport(
origin: String?,
onlyIfSilent: Boolean = false,
block: () -> T,
): Result<T> =
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() {
Comment thread
david-allison marked this conversation as resolved.
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)
1 change: 1 addition & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading