Merge branch 'master' into devel
This commit is contained in:
@@ -82,7 +82,7 @@ afterEvaluate {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:79e1d59482') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:f80b586081') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.3'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.4'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.5'
|
||||
implementation 'androidx.transition:transition-ktx:1.5.1'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6'
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
|
||||
class DialogErrorObserver(
|
||||
@@ -32,7 +33,7 @@ class DialogErrorObserver(
|
||||
dialogBuilder.setPositiveButton(ExceptionResolver.getResolveStringId(value), listener)
|
||||
} else if (value is ParseException) {
|
||||
val fm = fragmentManager
|
||||
if (fm != null) {
|
||||
if (fm != null && value.isSerializable()) {
|
||||
dialogBuilder.setPositiveButton(R.string.details) { _, _ ->
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
|
||||
@@ -33,7 +34,7 @@ class SnackbarErrorObserver(
|
||||
}
|
||||
} else if (value is ParseException) {
|
||||
val fm = fragmentManager
|
||||
if (fm != null) {
|
||||
if (fm != null && value.isSerializable()) {
|
||||
snackbar.setAction(R.string.details) {
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.io
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.util.Objects
|
||||
|
||||
class NullOutputStream : OutputStream() {
|
||||
|
||||
override fun write(b: Int) = Unit
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
Objects.checkFromIndexSize(off, len, b.size)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T?
|
||||
return BundleCompat.getParcelable(this, key, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : Parcelable> Bundle.requireParcelable(key: String): T = checkNotNull(getParcelableCompat(key)) {
|
||||
"Parcelable of type \"${T::class.java.name}\" not found at \"$key\""
|
||||
}
|
||||
|
||||
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? {
|
||||
return IntentCompat.getParcelableExtra(this, key, T::class.java)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.io.NullOutputStream
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED
|
||||
@@ -38,6 +39,7 @@ import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
|
||||
import java.net.ConnectException
|
||||
import java.net.NoRouteToHostException
|
||||
import java.io.ObjectOutputStream
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import java.util.Locale
|
||||
@@ -203,3 +205,9 @@ fun Throwable.isWebViewUnavailable(): Boolean {
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT)
|
||||
|
||||
fun Throwable.isSerializable() = runCatching {
|
||||
val oos = ObjectOutputStream(NullOutputStream())
|
||||
oos.writeObject(this)
|
||||
oos.flush()
|
||||
}.isSuccess
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isNetworkError
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
|
||||
@@ -38,7 +39,7 @@ class DetailsErrorObserver(
|
||||
|
||||
value is ParseException -> {
|
||||
val fm = fragmentManager
|
||||
if (fm != null) {
|
||||
if (fm != null && value.isSerializable()) {
|
||||
snackbar.setAction(R.string.details) {
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package org.koitharu.kotatsu.download.ui.worker
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.collection.MutableObjectLongMap
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class DownloadSlowdownDispatcher(
|
||||
@Singleton
|
||||
class DownloadSlowdownDispatcher @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val defaultDelay: Long,
|
||||
) {
|
||||
private val timeMap = MutableObjectLongMap<MangaSource>()
|
||||
private val defaultDelay = 1_600L
|
||||
|
||||
suspend fun delay(source: MangaSource) {
|
||||
val repo = mangaRepositoryFactory.create(source) as? ParserMangaRepository ?: return
|
||||
@@ -19,11 +23,11 @@ class DownloadSlowdownDispatcher(
|
||||
}
|
||||
val lastRequest = synchronized(timeMap) {
|
||||
val res = timeMap.getOrDefault(source, 0L)
|
||||
timeMap[source] = System.currentTimeMillis()
|
||||
timeMap[source] = SystemClock.elapsedRealtime()
|
||||
res
|
||||
}
|
||||
if (lastRequest != 0L) {
|
||||
delay(lastRequest + defaultDelay - System.currentTimeMillis())
|
||||
delay(lastRequest + defaultDelay - SystemClock.elapsedRealtime())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val settings: AppSettings,
|
||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||
private val slowdownDispatcher: DownloadSlowdownDispatcher,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
notificationFactoryFactory: DownloadNotificationFactory.Factory,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
@@ -108,7 +109,6 @@ class DownloadWorker @AssistedInject constructor(
|
||||
private val task = DownloadTask(params.inputData)
|
||||
private val notificationFactory = notificationFactoryFactory.create(uuid = params.id, isSilent = task.isSilent)
|
||||
private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
private val slowdownDispatcher = DownloadSlowdownDispatcher(mangaRepositoryFactory, SLOWDOWN_DELAY)
|
||||
|
||||
@Volatile
|
||||
private var lastPublishedState: DownloadState? = null
|
||||
@@ -535,7 +535,10 @@ class DownloadWorker @AssistedInject constructor(
|
||||
const val MAX_PAGES_PARALLELISM = 4
|
||||
const val DOWNLOAD_ERROR_DELAY = 2_000L
|
||||
const val MAX_RETRY_DELAY = 7_200_000L // 2 hours
|
||||
const val SLOWDOWN_DELAY = 200L
|
||||
const val MANGA_ID = "manga_id"
|
||||
const val CHAPTERS_IDS = "chapters"
|
||||
const val IS_SILENT = "silent"
|
||||
const val START_PAUSED = "paused"
|
||||
const val TAG = "download"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@ package org.koitharu.kotatsu.main.domain
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.network.HttpException
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageResult
|
||||
import okio.FileNotFoundException
|
||||
import org.jsoup.HttpStatusException
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
@@ -17,13 +14,10 @@ import org.koitharu.kotatsu.core.util.ext.bookmarkKey
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaKey
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.net.UnknownHostException
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLException
|
||||
|
||||
class CoverRestoreInterceptor @Inject constructor(
|
||||
private val dataRepository: MangaDataRepository,
|
||||
@@ -118,11 +112,6 @@ class CoverRestoreInterceptor @Inject constructor(
|
||||
}
|
||||
|
||||
private fun Throwable.shouldRestore(): Boolean {
|
||||
return this is HttpException
|
||||
|| this is HttpStatusException
|
||||
|| this is SSLException
|
||||
|| this is ParseException
|
||||
|| this is UnknownHostException
|
||||
|| this is FileNotFoundException
|
||||
return this is Exception // any Exception but not Error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
||||
import org.koitharu.kotatsu.core.util.ext.use
|
||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadSlowdownDispatcher
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -78,6 +79,7 @@ class PageLoader @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
private val downloadSlowdownDispatcher: DownloadSlowdownDispatcher,
|
||||
) {
|
||||
|
||||
val loaderScope = RetainedLifecycleCoroutineScope(lifecycle) + InternalErrorHandler() + Dispatchers.Default
|
||||
@@ -126,7 +128,7 @@ class PageLoader @Inject constructor(
|
||||
} else if (task?.isCancelled == false) {
|
||||
return task
|
||||
}
|
||||
task = loadPageAsyncImpl(page, force)
|
||||
task = loadPageAsyncImpl(page, skipCache = force, isPrefetch = false)
|
||||
synchronized(tasks) {
|
||||
tasks[page.id] = task
|
||||
}
|
||||
@@ -185,7 +187,7 @@ class PageLoader @Inject constructor(
|
||||
val page = prefetchQueue.pollFirst() ?: return@launch
|
||||
if (cache.get(page.url) == null) {
|
||||
synchronized(tasks) {
|
||||
tasks[page.id] = loadPageAsyncImpl(page, false)
|
||||
tasks[page.id] = loadPageAsyncImpl(page, skipCache = false, isPrefetch = true)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
@@ -193,7 +195,11 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPageAsyncImpl(page: MangaPage, skipCache: Boolean): ProgressDeferred<Uri, Float> {
|
||||
private fun loadPageAsyncImpl(
|
||||
page: MangaPage,
|
||||
skipCache: Boolean,
|
||||
isPrefetch: Boolean,
|
||||
): ProgressDeferred<Uri, Float> {
|
||||
val progress = MutableStateFlow(PROGRESS_UNDEFINED)
|
||||
val deferred = loaderScope.async {
|
||||
if (!skipCache) {
|
||||
@@ -201,7 +207,7 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
counter.incrementAndGet()
|
||||
try {
|
||||
loadPageImpl(page, progress)
|
||||
loadPageImpl(page, progress, isPrefetch)
|
||||
} finally {
|
||||
if (counter.decrementAndGet() == 0) {
|
||||
onIdle()
|
||||
@@ -221,7 +227,11 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): Uri = semaphore.withPermit {
|
||||
private suspend fun loadPageImpl(
|
||||
page: MangaPage,
|
||||
progress: MutableStateFlow<Float>,
|
||||
isPrefetch: Boolean,
|
||||
): Uri = semaphore.withPermit {
|
||||
val pageUrl = getPageUrl(page)
|
||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" }
|
||||
val uri = Uri.parse(pageUrl)
|
||||
@@ -234,6 +244,9 @@ class PageLoader @Inject constructor(
|
||||
|
||||
uri.isFileUri() -> uri
|
||||
else -> {
|
||||
if (isPrefetch) {
|
||||
downloadSlowdownDispatcher.delay(page.source)
|
||||
}
|
||||
val request = createPageRequest(pageUrl, page.source)
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||
response.requireBody().withProgress(progress).use {
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
@@ -154,6 +155,7 @@ open class PageHolder(
|
||||
bindingInfo.buttonRetry.setText(
|
||||
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
|
||||
)
|
||||
bindingInfo.buttonErrorDetails.isVisible = e.isSerializable()
|
||||
bindingInfo.layoutError.isVisible = true
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.GoneOnInvisibleListener
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
@@ -128,6 +129,7 @@ class WebtoonHolder(
|
||||
bindingInfo.buttonRetry.setText(
|
||||
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
|
||||
)
|
||||
bindingInfo.buttonErrorDetails.isVisible = e.isSerializable()
|
||||
bindingInfo.layoutError.isVisible = true
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user