Fix pages loading issues

(cherry picked from commit 5bccc595a8)
master
Koitharu 2 years ago
parent d59b0626bc
commit 9942ad5e56
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -136,7 +136,7 @@ dependencies {
implementation 'io.coil-kt:coil-base:2.7.0' implementation 'io.coil-kt:coil-base:2.7.0'
implementation 'io.coil-kt:coil-svg:2.7.0' implementation 'io.coil-kt:coil-svg:2.7.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:e04098de68' implementation 'com.github.KotatsuApp:subsampling-scale-image-view:ac7360c5e3'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'

@ -26,8 +26,10 @@ class ProgressResponseBody(
override fun contentType(): MediaType? = delegate.contentType() override fun contentType(): MediaType? = delegate.contentType()
override fun source(): BufferedSource { override fun source(): BufferedSource {
return bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also { return bufferedSource ?: synchronized(this) {
bufferedSource = it bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also {
bufferedSource = it
}
} }
} }

@ -104,10 +104,9 @@ class MangaPageFetcher(
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw HttpException(response) throw HttpException(response)
} }
val body = response.requireBody()
val mimeType = response.mimeType val mimeType = response.mimeType
val file = body.use { val file = response.requireBody().use {
pagesCache.put(pageUrl, it.source()) pagesCache.put(pageUrl, it.source(), mimeType)
} }
SourceResult( SourceResult(
source = ImageSource( source = ImageSource(

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.data
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.StatFs import android.os.StatFs
import android.webkit.MimeTypeMap
import com.tomclaw.cache.DiskLruCache import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -15,7 +16,7 @@ import okio.use
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.compressToPNG import org.koitharu.kotatsu.core.util.ext.compressToPNG
import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.subdir
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
@ -24,6 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File import java.io.File
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -50,15 +52,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
}.getOrThrow() }.getOrThrow()
} }
suspend fun get(url: String): File? { suspend fun get(url: String): File? = withContext(Dispatchers.IO) {
val cache = lruCache.get() val cache = lruCache.get()
return runInterruptible(Dispatchers.IO) { runInterruptible {
cache.get(url)?.takeIfReadable() cache.get(url)?.takeIfReadable()
} }
} }
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) { suspend fun put(url: String, source: Source, mimeType: String?): File = withContext(Dispatchers.IO) {
val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) val file = createBufferFile(url, mimeType)
try { try {
val bytes = file.sink(append = false).buffer().use { val bytes = file.sink(append = false).buffer().use {
it.writeAllCancellable(source) it.writeAllCancellable(source)
@ -66,17 +68,23 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
if (bytes == 0L) { if (bytes == 0L) {
throw NoDataReceivedException(url) throw NoDataReceivedException(url)
} }
lruCache.get().put(url, file) val cache = lruCache.get()
runInterruptible {
cache.put(url, file)
}
} finally { } finally {
file.delete() file.delete()
} }
} }
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) { suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) val file = createBufferFile(url, "image/png")
try { try {
bitmap.compressToPNG(file) bitmap.compressToPNG(file)
lruCache.get().put(url, file) val cache = lruCache.get()
runInterruptible {
cache.put(url, file)
}
} finally { } finally {
file.delete() file.delete()
} }
@ -90,12 +98,24 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
} }
private suspend fun getAvailableSize(): Long = runCatchingCancellable { private suspend fun getAvailableSize(): Long = runCatchingCancellable {
val statFs = StatFs(cacheDir.get().absolutePath) val dir = cacheDir.get()
statFs.availableBytes runInterruptible(Dispatchers.IO) {
val statFs = StatFs(dir.absolutePath)
statFs.availableBytes
}
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
}.getOrDefault(SIZE_DEFAULT) }.getOrDefault(SIZE_DEFAULT)
private suspend fun createBufferFile(url: String, mimeType: String?): File {
val ext = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }
?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
val cacheDir = cacheDir.get()
val rootDir = checkNotNull(cacheDir.parentFile) { "Cannot get parent for ${cacheDir.absolutePath}" }
val name = UUID.randomUUID().toString() + "." + ext
return File(rootDir, name)
}
private companion object { private companion object {
val SIZE_MIN val SIZE_MIN

@ -3,8 +3,10 @@ package org.koitharu.kotatsu.reader.domain
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
@ -56,6 +58,7 @@ import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
@ -149,9 +152,13 @@ class PageLoader @Inject constructor(
cache.put(uri.toString(), bitmap).toUri() cache.put(uri.toString(), bitmap).toUri()
} else { } else {
val file = uri.toFile() val file = uri.toFile()
context.ensureRamAtLeast(file.length() * 2)
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath)) context.ensureRamAtLeast(file.length() * 2)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
} else {
checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath))
}
}.use { image -> }.use { image ->
image.compressToPNG(file) image.compressToPNG(file)
} }
@ -235,7 +242,7 @@ class PageLoader @Inject constructor(
val request = createPageRequest(pageUrl, page.source) val request = createPageRequest(pageUrl, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
response.requireBody().withProgress(progress).use { response.requireBody().withProgress(progress).use {
cache.put(pageUrl, it.source()) cache.put(pageUrl, it.source(), response.mimeType)
} }
}.toUri() }.toUri()
} }

@ -152,6 +152,7 @@ class PageHolderDelegate(
} catch (ce: CancellationException) { } catch (ce: CancellationException) {
throw ce throw ce
} catch (e2: Throwable) { } catch (e2: Throwable) {
e2.printStackTrace()
e.addSuppressed(e2) e.addSuppressed(e2)
state = State.ERROR state = State.ERROR
callback.onError(e) callback.onError(e)

Loading…
Cancel
Save