From ce97c8f7d9fd966b02632a8e83df7e94db438feb Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 2 Jul 2022 11:02:03 +0300 Subject: [PATCH] Ability to report non-fatal exceptions --- .gitignore | 1 + .idea/render.experimental.xml | 6 -- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 1 + .../kotatsu/base/ui/widgets/FadingSnackbar.kt | 87 ++++++++++++++----- .../kotatsu/details/ui/DetailsActivity.kt | 20 ++++- .../kotatsu/details/ui/DetailsViewModel.kt | 2 +- .../details/ui/MangaDetailsDelegate.kt | 5 ++ .../kotatsu/reader/ui/ReaderActivity.kt | 9 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 5 +- .../ext/{CommonExt.kt => ThrowableExt.kt} | 6 +- .../res/layout/fading_snackbar_layout.xml | 19 ++-- app/src/main/res/values/strings.xml | 1 + 12 files changed, 118 insertions(+), 44 deletions(-) delete mode 100644 .idea/render.experimental.xml rename app/src/main/java/org/koitharu/kotatsu/utils/ext/{CommonExt.kt => ThrowableExt.kt} (87%) diff --git a/.gitignore b/.gitignore index 9b63e14e7..5611db9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /.idea/kotlinScripting.xml /.idea/deploymentTargetDropDown.xml /.idea/androidTestResultsUserPreferences.xml +/.idea/render.experimental.xml .DS_Store /build /captures diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml deleted file mode 100644 index 5cad4e0be..000000000 --- a/.idea/render.experimental.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 8035a83fc..a1e9ace1d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -92,6 +92,7 @@ class KotatsuApp : Application() { ReportField.PHONE_MODEL, ReportField.CRASH_CONFIGURATION, ReportField.STACK_TRACE, + ReportField.CUSTOM_DATA, ReportField.SHARED_PREFERENCES, ) dialog { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt index 9c85787a7..909252b48 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt @@ -17,19 +17,28 @@ package org.koitharu.kotatsu.base.ui.widgets import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater -import android.widget.Button import android.widget.FrameLayout -import android.widget.TextView +import androidx.annotation.ColorInt import androidx.annotation.StringRes +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.postDelayed -import org.koitharu.kotatsu.R +import com.google.android.material.color.MaterialColors +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel +import com.google.android.material.snackbar.Snackbar +import org.koitharu.kotatsu.databinding.FadingSnackbarLayoutBinding +import org.koitharu.kotatsu.utils.ext.getThemeColorStateList +import com.google.android.material.R as materialR private const val ENTER_DURATION = 300L private const val EXIT_DURATION = 200L -private const val SHORT_DURATION = 1_500L -private const val LONG_DURATION = 2_750L +private const val SHORT_DURATION_MS = 1_500L +private const val LONG_DURATION_MS = 2_750L + /** * A custom snackbar implementation allowing more control over placement and entry/exit animations. * @@ -40,16 +49,13 @@ private const val LONG_DURATION = 2_750L class FadingSnackbar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + defStyleAttr: Int = 0, ) : FrameLayout(context, attrs, defStyleAttr) { - private val message: TextView - private val action: Button + private val binding = FadingSnackbarLayoutBinding.inflate(LayoutInflater.from(context), this) init { - val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true) - message = view.findViewById(R.id.snackbar_text) - action = view.findViewById(R.id.snackbar_action) + binding.snackbarLayout.background = createThemedBackground() } fun dismiss() { @@ -62,33 +68,66 @@ class FadingSnackbar @JvmOverloads constructor( } fun show( - messageText: CharSequence? = null, - @StringRes actionId: Int? = null, - longDuration: Boolean = true, - actionClick: () -> Unit = { dismiss() }, - dismissListener: () -> Unit = { } + messageText: CharSequence?, + @StringRes actionId: Int = 0, + duration: Int = Snackbar.LENGTH_SHORT, + onActionClick: (FadingSnackbar.() -> Unit)? = null, + onDismiss: (() -> Unit)? = null, ) { - message.text = messageText - if (actionId != null) { - action.run { + binding.snackbarText.text = messageText + if (actionId != 0) { + with(binding.snackbarAction) { visibility = VISIBLE text = context.getString(actionId) setOnClickListener { - actionClick() + onActionClick?.invoke(this@FadingSnackbar) ?: dismiss() } } } else { - action.visibility = GONE + binding.snackbarAction.visibility = GONE } alpha = 0f visibility = VISIBLE animate() .alpha(1f) .duration = ENTER_DURATION - val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION - postDelayed(showDuration) { + if (duration == Snackbar.LENGTH_INDEFINITE) { + return + } + val durationMs = ENTER_DURATION + if (duration == Snackbar.LENGTH_LONG) LONG_DURATION_MS else SHORT_DURATION_MS + postDelayed(durationMs) { dismiss() - dismissListener() + onDismiss?.invoke() } } + + private fun createThemedBackground(): Drawable { + val backgroundColor = MaterialColors.layer(this, materialR.attr.colorSurface, materialR.attr.colorOnSurface, 1f) + val shapeAppearanceModel = ShapeAppearanceModel.builder( + context, + materialR.style.ShapeAppearance_Material3_Corner_ExtraSmall, + 0 + ).build() + val background = createMaterialShapeDrawableBackground( + backgroundColor, + shapeAppearanceModel, + ) + val backgroundTint = context.getThemeColorStateList(materialR.attr.colorSurfaceInverse) + return if (backgroundTint != null) { + val wrappedDrawable = DrawableCompat.wrap(background) + DrawableCompat.setTintList(wrappedDrawable, backgroundTint) + wrappedDrawable + } else { + DrawableCompat.wrap(background) + } + } + + private fun createMaterialShapeDrawableBackground( + @ColorInt backgroundColor: Int, + shapeAppearanceModel: ShapeAppearanceModel, + ): MaterialShapeDrawable { + val background = MaterialShapeDrawable(shapeAppearanceModel) + background.fillColor = ColorStateList.valueOf(backgroundColor) + return background + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index dc5e6a513..df529038e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -21,9 +21,11 @@ import androidx.core.view.updatePadding import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.launch +import org.acra.ktx.sendWithAcra import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -37,6 +39,7 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapNotNullToSet @@ -81,7 +84,7 @@ class DetailsActivity : viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) viewModel.onError.observe(this, ::onError) viewModel.onShowToast.observe(this) { - binding.snackbar.show(messageText = getString(it), longDuration = false) + binding.snackbar.show(messageText = getString(it)) } registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE)) @@ -114,6 +117,21 @@ class DetailsActivity : Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() finishAfterTransition() } + e is ParseException || e is IllegalArgumentException || e is IllegalStateException -> { + binding.snackbar.show( + messageText = e.getDisplayMessage(resources), + actionId = R.string.report, + duration = if (viewModel.manga.value?.chapters == null) { + Snackbar.LENGTH_INDEFINITE + } else { + Snackbar.LENGTH_LONG + }, + onActionClick = { + e.sendWithAcra() + dismiss() + } + ) + } else -> { binding.snackbar.show(e.getDisplayMessage(resources)) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 757c70f61..0cab2ea03 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* @@ -30,6 +29,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import java.io.IOException class DetailsViewModel( intent: MangaIntent, diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt index 07f03dbda..c6c45ecc1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui import androidx.core.os.LocaleListCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import org.acra.ACRA import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException @@ -13,6 +14,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource @@ -20,6 +22,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.setCurrentManga class MangaDetailsDelegate( private val intent: MangaIntent, @@ -32,6 +35,7 @@ class MangaDetailsDelegate( private val mangaData = MutableStateFlow(intent.manga) val selectedBranch = MutableStateFlow(null) + // Remote manga for saved and saved for remote val relatedManga = MutableStateFlow(null) val manga: StateFlow @@ -41,6 +45,7 @@ class MangaDetailsDelegate( suspend fun doLoad() { var manga = mangaDataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") + ACRA.setCurrentManga(manga) mangaData.value = manga manga = MangaRepository(manga.source).getDetails(manga) // find default branch diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index e4a29b948..dfd3af976 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.acra.ktx.sendWithAcra import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -214,6 +215,8 @@ class ReaderActivity : val resolveTextId = ExceptionResolver.getResolveStringId(e) if (resolveTextId != 0) { dialog.setPositiveButton(resolveTextId, listener) + } else { + dialog.setPositiveButton(R.string.report, listener) } dialog.show() } @@ -368,7 +371,11 @@ class ReaderActivity : override fun onClick(dialog: DialogInterface?, which: Int) { if (which == DialogInterface.BUTTON_POSITIVE) { dialog?.dismiss() - tryResolve(exception) + if (ExceptionResolver.canResolve(exception)) { + tryResolve(exception) + } else { + exception.sendWithAcra() + } } else { onCancel(dialog) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 931461de0..bb8555941 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -6,9 +6,9 @@ import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.util.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import org.acra.ACRA import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent @@ -32,6 +32,8 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.utils.ext.setCurrentManga +import java.util.* private const val BOUNDS_PAGE_OFFSET = 2 private const val PAGES_TRIM_THRESHOLD = 120 @@ -257,6 +259,7 @@ class ReaderViewModel( private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") + ACRA.setCurrentManga(manga) mangaData.value = manga val repo = MangaRepository(manga.source) manga = repo.getDetails(manga) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt rename to app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt index dc6974749..a71f3b7b3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt @@ -1,12 +1,14 @@ package org.koitharu.kotatsu.utils.ext import android.content.res.Resources +import org.acra.ACRA import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.model.Manga import java.io.FileNotFoundException import java.net.SocketTimeoutException @@ -20,4 +22,6 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) { is SocketTimeoutException -> resources.getString(R.string.network_error) is WrongPasswordException -> resources.getString(R.string.wrong_password) else -> localizedMessage ?: resources.getString(R.string.error_occurred) -} \ No newline at end of file +} + +fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString()) \ No newline at end of file diff --git a/app/src/main/res/layout/fading_snackbar_layout.xml b/app/src/main/res/layout/fading_snackbar_layout.xml index 4c75ce8b9..0ebbb7bdd 100644 --- a/app/src/main/res/layout/fading_snackbar_layout.xml +++ b/app/src/main/res/layout/fading_snackbar_layout.xml @@ -1,5 +1,4 @@ - -