Skip to content
Closed
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
58 changes: 54 additions & 4 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ open class AnkiDroidApp :
/** An exception if AnkiDroidApp fails to load */
private var fatalInitializationError: FatalInitializationError? = null

private val nonFatalInitializationErrors = mutableListOf<NonFatalInitializationError>()

@LegacyNotifications("The widget triggers notifications by posting null to this, but we plan to stop relying on the widget")
private val notifications = MutableLiveData<Void?>()

Expand All @@ -98,6 +100,10 @@ open class AnkiDroidApp :
*/
@KotlinCleanup("analytics can be moved to attachBaseContext()")
override fun onCreate() {
val startupTimer = System.currentTimeMillis()

fun elapsed() = "${System.currentTimeMillis() - startupTimer}ms"

try {
Os.setenv("PLATFORM", syncPlatform(), false)
// enable debug logging of sync actions
Expand Down Expand Up @@ -135,6 +141,7 @@ open class AnkiDroidApp :
ChangeManager.subscribe(this)

CrashReportService.initialize(this)
Timber.plant(ProductionCrashReportingTree()) // or whichever applies
val logType = LogType.value
when (logType) {
LogType.DEBUG -> Timber.plant(DebugTree())
Expand All @@ -147,17 +154,20 @@ open class AnkiDroidApp :
LeakCanaryConfiguration.disable()
}
Timber.tag(TAG)
Timber.d("STARTUP [${elapsed()}] logging ready")
Timber.d("Startup - Application Start")
Timber.i("Timber config: $logType")

// analytics after ACRA, they both install UncaughtExceptionHandlers but Analytics chains while ACRA does not
UsageAnalytics.initialize(this)
Timber.d("STARTUP [${elapsed()}] analytics done")
if (BuildConfig.DEBUG) {
UsageAnalytics.setDryRun(true)
}

// Last in the UncaughtExceptionHandlers chain is our filter service
ThrowableFilterService.initialize()
Timber.d("STARTUP [${elapsed()}] throwable filter done")

applicationScope.launch {
Timber.i("AnkiDroidApp: listing debug info")
Expand Down Expand Up @@ -189,6 +199,7 @@ open class AnkiDroidApp :
setupNotificationChannels(applicationContext)

makeBackendUsable(this)
Timber.d("STARTUP [${elapsed()}] ← CHECK THIS ONE - backend ready")

// Configure WebView to allow file scheme pages to access cookies.
if (!acceptFileSchemeCookies()) {
Expand All @@ -200,14 +211,36 @@ open class AnkiDroidApp :
LanguageUtil.setDefaultBackendLanguages()

initializeAnkiDroidDirectory()
Timber.d("STARTUP [${elapsed()}] ← CHECK THIS ONE - directory ready")

if (Prefs.newReviewRemindersEnabled) {
Timber.i("Setting review reminder notifications if they have not already been set")
AlarmManagerService.scheduleAllNotifications(applicationContext)
try {
AlarmManagerService.scheduleAllNotifications(applicationContext)
} catch (e: Exception) {
Timber.e(e, "Failed to schedule review reminder notifications")
nonFatalInitializationErrors.add(
NonFatalInitializationError(
componentName = "Review Reminders",
exception = e,
),
)
// App continues — reminders won't work but nothing else breaks
}
} else {
// Register for notifications
Timber.i("AnkiDroidApp: Starting Services")
notifications.observeForever { NotificationService.triggerNotificationFor(this) }
try {
notifications.observeForever {
NotificationService.triggerNotificationFor(this)
}
} catch (e: Exception) {
Timber.e(e, "Failed to register notification observer")
nonFatalInitializationErrors.add(
NonFatalInitializationError(
componentName = "Notifications",
exception = e,
),
)
}
}

// listen for day rollover: time + timezone changes
Expand Down Expand Up @@ -262,8 +295,12 @@ open class AnkiDroidApp :

activityAgnosticDialogs = ActivityAgnosticDialogs.register(this)
TtsVoices.launchBuildLocalesJob()
Timber.d("STARTUP [${elapsed()}] tts voices job launched")
// enable {{tts-voices:}} field filter
TtsVoicesFieldFilter.ensureApplied()
Timber.d("STARTUP [${elapsed()}] tts field filter done")

Timber.d("STARTUP [${elapsed()}] TOTAL - onCreate complete")
}

/**
Expand Down Expand Up @@ -519,6 +556,9 @@ open class AnkiDroidApp :
/** (optional) set if an unrecoverable error occurs during Application startup */
val fatalError: FatalInitializationError?
get() = instance.fatalInitializationError

val nonFatalStartupErrors: List<NonFatalInitializationError>
get() = instance.nonFatalInitializationErrors
}
}

Expand Down Expand Up @@ -549,3 +589,13 @@ sealed class FatalInitializationError {
is StorageError -> error.infoUri?.toUri()
}
}

/**
* Errors which occurred during startup that are recoverable -
* the app can continue, but some features may be unavailable.
* The user should be notified so they can report the issue.
*/
data class NonFatalInitializationError(
val componentName: String,
val exception: Exception,
)
31 changes: 31 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ open class DeckPicker :
)

setupFlows()
showNonFatalStartupErrors()
}

override fun setupBackPressedCallbacks() {
Expand Down Expand Up @@ -1004,6 +1005,36 @@ open class DeckPicker :
}
}

/**
* Shows a dialog informing the user of any nonfatal errors that occurred during app startup.
* These are errors where the app can continue running, but some features may be unavailable.
*
* Also reports each error to the crash reporting service so developers are notified.
*
* @see AnkiDroidApp.nonFatalStartupErrors
* @see NonFatalInitializationError
*/
private fun showNonFatalStartupErrors() {
val errors = AnkiDroidApp.nonFatalStartupErrors
if (errors.isEmpty()) return

errors.forEach { error ->
Timber.e(error.exception, "NonFatal startup error: ${error.componentName}")
CrashReportService.sendExceptionReport(
error.exception,
"NonFatalStartupError: ${error.componentName}",
)
}

val errorList = errors.joinToString("\n") { " • ${it.componentName}" }

AlertDialog.Builder(this).show {
title(R.string.startup_error_title)
message(text = getString(R.string.startup_error_message, errorList))
positiveButton(R.string.dialog_ok)
}
}

private fun showDirectoryNotAccessibleDialog() {
val contentView =
TextView(this).apply {
Expand Down
4 changes: 4 additions & 0 deletions AnkiDroid/src/main/res/values/01-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,8 @@

<!-- Multiple profile -->
<string name="add_profile">Add profile</string>

<!-- Non-FatalStartup Failure -->
<string name="startup_error_title">Some features couldn\'t load</string>
<string name="startup_error_message">The following features failed to start:\n\n%1$s\n\nPlease restart the app. If this continues, contact support.</string>
</resources>
Loading