Ability to report non-fatal exceptions
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@
|
||||
/.idea/kotlinScripting.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/androidTestResultsUserPreferences.xml
|
||||
/.idea/render.experimental.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
6
.idea/render.experimental.xml
generated
6
.idea/render.experimental.xml
generated
@@ -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>
|
||||
@@ -92,6 +92,7 @@ class KotatsuApp : Application() {
|
||||
ReportField.PHONE_MODEL,
|
||||
ReportField.CRASH_CONFIGURATION,
|
||||
ReportField.STACK_TRACE,
|
||||
ReportField.CUSTOM_DATA,
|
||||
ReportField.SHARED_PREFERENCES,
|
||||
)
|
||||
dialog {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user