Ability to report non-fatal exceptions

This commit is contained in:
Koitharu
2022-07-02 11:02:03 +03:00
parent a48abc56dd
commit ce97c8f7d9
12 changed files with 118 additions and 44 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@
/.idea/kotlinScripting.xml
/.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/render.experimental.xml
.DS_Store
/build
/captures

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="quality" value="0.25" />
</component>
</project>

View File

@@ -92,6 +92,7 @@ class KotatsuApp : Application() {
ReportField.PHONE_MODEL,
ReportField.CRASH_CONFIGURATION,
ReportField.STACK_TRACE,
ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES,
)
dialog {

View File

@@ -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
}
}

View File

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

View File

@@ -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,

View File

@@ -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<String?>(null)
// Remote manga for saved and saved for remote
val relatedManga = MutableStateFlow<Manga?>(null)
val manga: StateFlow<Manga?>
@@ -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

View File

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

View File

@@ -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)

View File

@@ -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)
}
}
fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString())

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2018 Google LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,15 +21,17 @@
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/snackbar_layout"
style="?attr/snackbarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:background="@drawable/fading_snackbar_background"
android:theme="@style/ThemeOverlay.Kotatsu"
android:background="@drawable/design_snackbar_background"
android:elevation="8dp">
<TextView
android:id="@+id/snackbar_text"
style="?attr/snackbarTextViewStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
@@ -39,22 +40,22 @@
android:maxLines="4"
android:padding="@dimen/margin_normal"
android:textAlignment="viewStart"
android:textColor="@android:color/white"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
android:textColor="@android:color/white"
tools:text="Look at all the wonderful snack bar text..." />
<Button
android:id="@+id/snackbar_action"
style="?borderlessButtonStyle"
style="?attr/snackbarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|end"
android:paddingEnd="@dimen/margin_normal"
android:paddingStart="@dimen/margin_normal"
android:paddingEnd="@dimen/margin_normal"
android:visibility="gone"
tools:targetApi="o"
tools:text="Action"
tools:visibility="visible"
tools:targetApi="o" />
tools:visibility="visible" />
</LinearLayout>

View File

@@ -304,4 +304,5 @@
<string name="use_fingerprint">Use fingerprint if available</string>
<string name="appwidget_shelf_description">Manga from your favourites</string>
<string name="appwidget_recent_description">Your recently read manga</string>
<string name="report">Report</string>
</resources>