Ability to report non-fatal exceptions

pull/125/head^2
Koitharu 4 years ago
parent a48abc56dd
commit ce97c8f7d9
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

1
.gitignore vendored

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

@ -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.PHONE_MODEL,
ReportField.CRASH_CONFIGURATION, ReportField.CRASH_CONFIGURATION,
ReportField.STACK_TRACE, ReportField.STACK_TRACE,
ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES, ReportField.SHARED_PREFERENCES,
) )
dialog { dialog {

@ -17,19 +17,28 @@
package org.koitharu.kotatsu.base.ui.widgets package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Button
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.TextView import androidx.annotation.ColorInt
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.postDelayed 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 ENTER_DURATION = 300L
private const val EXIT_DURATION = 200L private const val EXIT_DURATION = 200L
private const val SHORT_DURATION = 1_500L private const val SHORT_DURATION_MS = 1_500L
private const val LONG_DURATION = 2_750L private const val LONG_DURATION_MS = 2_750L
/** /**
* A custom snackbar implementation allowing more control over placement and entry/exit animations. * 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( class FadingSnackbar @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
private val message: TextView private val binding = FadingSnackbarLayoutBinding.inflate(LayoutInflater.from(context), this)
private val action: Button
init { init {
val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true) binding.snackbarLayout.background = createThemedBackground()
message = view.findViewById(R.id.snackbar_text)
action = view.findViewById(R.id.snackbar_action)
} }
fun dismiss() { fun dismiss() {
@ -62,33 +68,66 @@ class FadingSnackbar @JvmOverloads constructor(
} }
fun show( fun show(
messageText: CharSequence? = null, messageText: CharSequence?,
@StringRes actionId: Int? = null, @StringRes actionId: Int = 0,
longDuration: Boolean = true, duration: Int = Snackbar.LENGTH_SHORT,
actionClick: () -> Unit = { dismiss() }, onActionClick: (FadingSnackbar.() -> Unit)? = null,
dismissListener: () -> Unit = { } onDismiss: (() -> Unit)? = null,
) { ) {
message.text = messageText binding.snackbarText.text = messageText
if (actionId != null) { if (actionId != 0) {
action.run { with(binding.snackbarAction) {
visibility = VISIBLE visibility = VISIBLE
text = context.getString(actionId) text = context.getString(actionId)
setOnClickListener { setOnClickListener {
actionClick() onActionClick?.invoke(this@FadingSnackbar) ?: dismiss()
} }
} }
} else { } else {
action.visibility = GONE binding.snackbarAction.visibility = GONE
} }
alpha = 0f alpha = 0f
visibility = VISIBLE visibility = VISIBLE
animate() animate()
.alpha(1f) .alpha(1f)
.duration = ENTER_DURATION .duration = ENTER_DURATION
val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION if (duration == Snackbar.LENGTH_INDEFINITE) {
postDelayed(showDuration) { return
}
val durationMs = ENTER_DURATION + if (duration == Snackbar.LENGTH_LONG) LONG_DURATION_MS else SHORT_DURATION_MS
postDelayed(durationMs) {
dismiss() 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.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ktx.sendWithAcra
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf 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.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.download.ui.service.DownloadService 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
@ -81,7 +84,7 @@ class DetailsActivity :
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError) viewModel.onError.observe(this, ::onError)
viewModel.onShowToast.observe(this) { 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)) registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
@ -114,6 +117,21 @@ class DetailsActivity :
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition() 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 -> { else -> {
binding.snackbar.show(e.getDisplayMessage(resources)) binding.snackbar.show(e.getDisplayMessage(resources))
} }

@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* 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.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.IOException
class DetailsViewModel( class DetailsViewModel(
intent: MangaIntent, intent: MangaIntent,

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.acra.ACRA
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException 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.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource 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.parsers.util.toTitleCase
import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.setCurrentManga
class MangaDetailsDelegate( class MangaDetailsDelegate(
private val intent: MangaIntent, private val intent: MangaIntent,
@ -32,6 +35,7 @@ class MangaDetailsDelegate(
private val mangaData = MutableStateFlow(intent.manga) private val mangaData = MutableStateFlow(intent.manga)
val selectedBranch = MutableStateFlow<String?>(null) val selectedBranch = MutableStateFlow<String?>(null)
// Remote manga for saved and saved for remote // Remote manga for saved and saved for remote
val relatedManga = MutableStateFlow<Manga?>(null) val relatedManga = MutableStateFlow<Manga?>(null)
val manga: StateFlow<Manga?> val manga: StateFlow<Manga?>
@ -41,6 +45,7 @@ class MangaDetailsDelegate(
suspend fun doLoad() { suspend fun doLoad() {
var manga = mangaDataRepository.resolveIntent(intent) var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga") ?: throw MangaNotFoundException("Cannot find manga")
ACRA.setCurrentManga(manga)
mangaData.value = manga mangaData.value = manga
manga = MangaRepository(manga.source).getDetails(manga) manga = MangaRepository(manga.source).getDetails(manga)
// find default branch // find default branch

@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.acra.ktx.sendWithAcra
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -214,6 +215,8 @@ class ReaderActivity :
val resolveTextId = ExceptionResolver.getResolveStringId(e) val resolveTextId = ExceptionResolver.getResolveStringId(e)
if (resolveTextId != 0) { if (resolveTextId != 0) {
dialog.setPositiveButton(resolveTextId, listener) dialog.setPositiveButton(resolveTextId, listener)
} else {
dialog.setPositiveButton(R.string.report, listener)
} }
dialog.show() dialog.show()
} }
@ -368,7 +371,11 @@ class ReaderActivity :
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
if (which == DialogInterface.BUTTON_POSITIVE) { if (which == DialogInterface.BUTTON_POSITIVE) {
dialog?.dismiss() dialog?.dismiss()
if (ExceptionResolver.canResolve(exception)) {
tryResolve(exception) tryResolve(exception)
} else {
exception.sendWithAcra()
}
} else { } else {
onCancel(dialog) onCancel(dialog)
} }

@ -6,9 +6,9 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.acra.ACRA
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent 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.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope 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 BOUNDS_PAGE_OFFSET = 2
private const val PAGES_TRIM_THRESHOLD = 120 private const val PAGES_TRIM_THRESHOLD = 120
@ -257,6 +259,7 @@ class ReaderViewModel(
private fun loadImpl() { private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga")
ACRA.setCurrentManga(manga)
mangaData.value = manga mangaData.value = manga
val repo = MangaRepository(manga.source) val repo = MangaRepository(manga.source)
manga = repo.getDetails(manga) manga = repo.getDetails(manga)

@ -1,12 +1,14 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import android.content.res.Resources import android.content.res.Resources
import org.acra.ACRA
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.Manga
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@ -21,3 +23,5 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
is WrongPasswordException -> resources.getString(R.string.wrong_password) is WrongPasswordException -> resources.getString(R.string.wrong_password)
else -> localizedMessage ?: resources.getString(R.string.error_occurred) 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 ~ Copyright 2018 Google LLC
~ ~
~ Licensed under the Apache License, Version 2.0 (the "License"); ~ Licensed under the Apache License, Version 2.0 (the "License");
@ -22,15 +21,17 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:id="@+id/snackbar_layout"
style="?attr/snackbarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:layout_margin="@dimen/margin_small"
android:background="@drawable/fading_snackbar_background" android:background="@drawable/design_snackbar_background"
android:theme="@style/ThemeOverlay.Kotatsu"
android:elevation="8dp"> android:elevation="8dp">
<TextView <TextView
android:id="@+id/snackbar_text" android:id="@+id/snackbar_text"
style="?attr/snackbarTextViewStyle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start" android:layout_gravity="center_vertical|start"
@ -39,22 +40,22 @@
android:maxLines="4" android:maxLines="4"
android:padding="@dimen/margin_normal" android:padding="@dimen/margin_normal"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textColor="@android:color/white"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Message" android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
android:textColor="@android:color/white"
tools:text="Look at all the wonderful snack bar text..." /> tools:text="Look at all the wonderful snack bar text..." />
<Button <Button
android:id="@+id/snackbar_action" android:id="@+id/snackbar_action"
style="?borderlessButtonStyle" style="?attr/snackbarButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical|end" android:layout_gravity="center_vertical|end"
android:paddingEnd="@dimen/margin_normal"
android:paddingStart="@dimen/margin_normal" android:paddingStart="@dimen/margin_normal"
android:paddingEnd="@dimen/margin_normal"
android:visibility="gone" android:visibility="gone"
tools:targetApi="o"
tools:text="Action" tools:text="Action"
tools:visibility="visible" tools:visibility="visible" />
tools:targetApi="o" />
</LinearLayout> </LinearLayout>

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