diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt index 2497234d9ba2..aa1e0b3fad8e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt @@ -24,7 +24,9 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModel import androidx.lifecycle.coroutineScope +import androidx.lifecycle.viewModelScope import anki.collection.Progress import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol @@ -41,6 +43,46 @@ import timber.log.Timber import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +/** + * Runs a suspend function that catches any uncaught errors and reports them to the user. + * Errors from the backend contain localized text that is often suitable to show to the user as-is. + * Other errors should ideally be handled in the block. + */ +fun CoroutineScope.launchCatching( + block: suspend () -> Unit, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + errorMessageHandler: suspend (String) -> Unit +): Job { + return launch(dispatcher) { + try { + block.invoke() + } catch (cancellationException: CancellationException) { + // CancellationException should be re-thrown to propagate it to the parent coroutine + throw cancellationException + } catch (backendException: BackendException) { + Timber.w(backendException) + val message = backendException.localizedMessage ?: backendException.toString() + errorMessageHandler.invoke(message) + } catch (exception: Exception) { + Timber.w(exception) + errorMessageHandler.invoke(exception.toString()) + } + } +} + +@Suppress("UNCHECKED_CAST") +fun ViewModel.launchCatching( + block: suspend T.() -> Unit, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + errorMessageHandler: suspend (String) -> Unit +): Job { + return viewModelScope.launchCatching( + { block.invoke(this as T) }, + dispatcher, + errorMessageHandler + ) +} + /** * Runs a suspend function that catches any uncaught errors and reports them to the user. * Errors from the backend contain localized text that is often suitable to show to the user as-is. diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt index f5bd3a17401c..768994f2be02 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt @@ -18,7 +18,6 @@ package com.ichi2.anki.previewer import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.google.android.material.color.MaterialColors.getColor @@ -26,20 +25,19 @@ import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.Flag import com.ichi2.anki.LanguageUtils +import com.ichi2.anki.launchCatching import com.ichi2.anki.servicelayer.MARKED_TAG import com.ichi2.anki.servicelayer.NoteService import com.ichi2.libanki.Card import com.ichi2.libanki.addPlayButtons import com.ichi2.themes.Themes import com.ichi2.utils.toRGBHex -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import net.ankiweb.rsdroid.BackendException import org.intellij.lang.annotations.Language import timber.log.Timber @@ -169,21 +167,9 @@ class PreviewerViewModel(mediaDir: String, private val selectedCardIds: LongArra showAnswerOrDisplayCard(currentIndex.value + 1) } - fun launchCatching(block: suspend PreviewerViewModel.() -> Unit) { - viewModelScope.launch(Dispatchers.IO) { - try { - block.invoke(this@PreviewerViewModel) - } catch (cancellationException: CancellationException) { - // CancellationException should be re-thrown to propagate it to the parent coroutine - throw cancellationException - } catch (backendException: BackendException) { - Timber.w(backendException) - val message = backendException.localizedMessage ?: backendException.toString() - onError.emit(message) - } catch (exception: Exception) { - Timber.w(exception) - onError.emit(exception.toString()) - } + fun launchCatching(block: suspend PreviewerViewModel.() -> Unit): Job { + return launchCatching(block, Dispatchers.IO) { message -> + onError.emit(message) } }