Improve pages loading #256

pull/260/head
Koitharu 4 years ago
parent b3eab1a2a0
commit b599cb33ff
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -26,7 +26,7 @@ Download APK directly from GitHub:
* Notifications about new chapters with updates feed * Notifications about new chapters with updates feed
* Shikimori integration (manga tracking) * Shikimori integration (manga tracking)
* Password/fingerprint protect access to the app * Password/fingerprint protect access to the app
* History and favourites synchronization across devices (coming soon) * History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
### Screenshots ### Screenshots

@ -28,6 +28,7 @@
android:fullBackupOnly="true" android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"

@ -115,4 +115,4 @@ class ZipOutput(
closeEntry() closeEntry()
return true return true
} }
} }

@ -17,7 +17,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -36,6 +35,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
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.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
@ -219,10 +219,8 @@ class DownloadManager @AssistedInject constructor(
val call = okHttp.newCall(request) val call = okHttp.newCall(request)
val file = File(destination, tempFileName) val file = File(destination, tempFileName)
val response = call.clone().await() val response = call.clone().await()
runInterruptible(Dispatchers.IO) { file.outputStream().use { out ->
file.outputStream().use { out -> checkNotNull(response.body).byteStream().copyToSuspending(out)
checkNotNull(response.body).byteStream().copyTo(out)
}
} }
return file return file
} }

@ -3,15 +3,21 @@ package org.koitharu.kotatsu.local.data
import android.content.Context import android.content.Context
import com.tomclaw.cache.DiskLruCache import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File import kotlinx.coroutines.Dispatchers
import java.io.InputStream import kotlinx.coroutines.Job
import javax.inject.Inject import kotlinx.coroutines.currentCoroutineContext
import javax.inject.Singleton import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) { class PagesCache @Inject constructor(@ApplicationContext context: Context) {
@ -26,37 +32,44 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
return lruCache.get(url)?.takeIfReadable() return lruCache.get(url)?.takeIfReadable()
} }
fun put(url: String, inputStream: InputStream): File { suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
val file = File(cacheDir, url.longHashCode().toString()) val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out -> try {
inputStream.copyTo(out) file.outputStream().use { out ->
inputStream.copyToSuspending(out)
}
lruCache.put(url, file)
} finally {
file.delete()
} }
val res = lruCache.put(url, file)
file.delete()
return res
} }
fun put( suspend fun put(
url: String, url: String,
inputStream: InputStream, inputStream: InputStream,
contentLength: Long, contentLength: Long,
progress: MutableStateFlow<Float>, progress: MutableStateFlow<Float>,
): File { ): File = withContext(Dispatchers.IO) {
val job = currentCoroutineContext()[Job]
val file = File(cacheDir, url.longHashCode().toString()) val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out -> try {
var bytesCopied: Long = 0 file.outputStream().use { out ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE) var bytesCopied: Long = 0
var bytes = inputStream.read(buffer) val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (bytes >= 0) { var bytes = inputStream.read(buffer)
out.write(buffer, 0, bytes) while (bytes >= 0) {
bytesCopied += bytes out.write(buffer, 0, bytes)
publishProgress(contentLength, bytesCopied, progress) bytesCopied += bytes
bytes = inputStream.read(buffer) job?.ensureActive()
publishProgress(contentLength, bytesCopied, progress)
bytes = inputStream.read(buffer)
job?.ensureActive()
}
} }
lruCache.put(url, file)
} finally {
file.delete()
} }
val res = lruCache.put(url, file)
file.delete()
return res
} }
private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) { private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) {

@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import java.io.File
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
@ -14,8 +13,10 @@ 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
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longOf import org.koitharu.kotatsu.utils.ext.longOf
import java.io.File
// TODO: Add support for chapters in cbz // TODO: Add support for chapters in cbz
// https://github.com/KotatsuApp/Kotatsu/issues/31 // https://github.com/KotatsuApp/Kotatsu/issues/31
@ -62,6 +63,7 @@ class DirMangaImporter(
file.isDirectory -> { file.isDirectory -> {
addPages(output, file, path + "/" + file.name, state) addPages(output, file, path + "/" + file.name, state)
} }
file.isFile -> { file.isFile -> {
val tempFile = file.asTempFile() val tempFile = file.asTempFile()
if (!state.hasCover) { if (!state.hasCover) {
@ -86,7 +88,7 @@ class DirMangaImporter(
"Cannot open input stream for $uri" "Cannot open input stream for $uri"
}.use { input -> }.use { input ->
file.outputStream().use { output -> file.outputStream().use { output ->
input.copyTo(output) input.copyToSuspending(output)
} }
} }
return file return file

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.local.domain.importer package org.koitharu.kotatsu.local.domain.importer
import android.net.Uri import android.net.Uri
import java.io.File
import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -11,7 +9,10 @@ import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.resolveName import org.koitharu.kotatsu.utils.ext.resolveName
import java.io.File
import java.io.IOException
class ZipMangaImporter( class ZipMangaImporter(
storageManager: LocalStorageManager, storageManager: LocalStorageManager,
@ -27,10 +28,10 @@ class ZipMangaImporter(
} }
val dest = File(getOutputDir(), name) val dest = File(getOutputDir(), name)
runInterruptible { runInterruptible {
contentResolver.openInputStream(uri)?.use { source -> contentResolver.openInputStream(uri)
dest.outputStream().use { output -> }?.use { source ->
source.copyTo(output) dest.outputStream().use { output ->
} source.copyToSuspending(output)
} }
} ?: throw IOException("Cannot open input stream: $uri") } ?: throw IOException("Cannot open input stream: $uri")
localMangaRepository.getFromFile(dest) localMangaRepository.getFromFile(dest)

@ -179,9 +179,12 @@ class PageLoader @Inject constructor(
val uri = Uri.parse(pageUrl) val uri = Uri.parse(pageUrl)
return if (uri.scheme == "cbz") { return if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart) ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment) }.use { zip ->
zip.getInputStream(entry).use { runInterruptible(Dispatchers.IO) {
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry)
}.use {
cache.put(pageUrl, it) cache.put(pageUrl, it)
} }
} }
@ -200,10 +203,8 @@ class PageLoader @Inject constructor(
val body = checkNotNull(response.body) { val body = checkNotNull(response.body) {
"Null response" "Null response"
} }
runInterruptible(Dispatchers.IO) { body.byteStream().use {
body.byteStream().use { cache.put(pageUrl, it, body.contentLength(), progress)
cache.put(pageUrl, it, body.contentLength(), progress)
}
} }
} }
} }

@ -6,10 +6,6 @@ import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toUri import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -20,6 +16,11 @@ import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
private const val MAX_FILENAME_LENGTH = 10 private const val MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png" private const val EXTENSION_FALLBACK = "png"
@ -48,12 +49,12 @@ class PageSaveHelper @Inject constructor(
} }
} }
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output -> contentResolver.openOutputStream(destination)
pageFile.inputStream().use { input -> }?.use { output ->
input.copyTo(output) pageFile.inputStream().use { input ->
} input.copyToSuspending(output)
} ?: throw IOException("Output stream is null") }
} } ?: throw IOException("Output stream is null")
return destination return destination
} }

@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -118,18 +119,20 @@ class PageHolderDelegate(
} }
} }
private suspend fun CoroutineScope.doLoad(data: MangaPage, force: Boolean) { private suspend fun doLoad(data: MangaPage, force: Boolean) {
state = State.LOADING state = State.LOADING
error = null error = null
callback.onLoadingStarted() callback.onLoadingStarted()
try { try {
val task = loader.loadPageAsync(data, force) val task = loader.loadPageAsync(data, force)
val progressObserver = observeProgress(this, task.progressAsFlow()) file = coroutineScope {
val file = task.await() val progressObserver = observeProgress(this, task.progressAsFlow())
progressObserver.cancel() val file = task.await()
this@PageHolderDelegate.file = file progressObserver.cancel()
file
}
state = State.LOADED state = State.LOADED
callback.onImageReady(file.toUri()) callback.onImageReady(checkNotNull(file).toUri())
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Throwable) { } catch (e: Throwable) {

@ -0,0 +1,27 @@
package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.io.OutputStream
suspend fun InputStream.copyToSuspending(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE
): Long = withContext(Dispatchers.IO) {
val job = currentCoroutineContext()[Job]
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
job?.ensureActive()
bytes = read(buffer)
job?.ensureActive()
}
bytesCopied
}
Loading…
Cancel
Save