diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt index 81e84e385..b237755a0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt @@ -17,6 +17,7 @@ import android.content.SyncResult import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.database.SQLException +import android.graphics.Bitmap import android.graphics.Color import android.net.Uri import android.os.Build @@ -29,6 +30,7 @@ import android.view.Window import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IntegerRes +import androidx.annotation.WorkerThread import androidx.core.app.ActivityOptionsCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -37,6 +39,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import androidx.work.CoroutineWorker import com.google.android.material.elevation.ElevationOverlayProvider +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay @@ -45,7 +48,9 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible import okio.IOException +import okio.use import org.json.JSONException import org.jsoup.internal.StringUtil.StringJoiner import org.koitharu.kotatsu.BuildConfig @@ -53,6 +58,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException +import java.io.File import kotlin.math.roundToLong val Context.activityManager: ActivityManager? @@ -230,3 +236,18 @@ fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT > } else { NotificationManagerCompat.from(this).areNotificationsEnabled() } + +@WorkerThread +suspend fun Bitmap.compressToPNG(output: File) = runInterruptible(Dispatchers.IO) { + output.outputStream().use { os -> + if (!compress(Bitmap.CompressFormat.PNG, 100, os)) { + throw IOException("Failed to encode bitmap into PNG format") + } + } +} + +fun Context.ensureRamAtLeast(requiredSize: Long) { + if (ramAvailable < requiredSize) { + throw IllegalStateException("Not enough free memory") + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt index ad52d2d26..4f81808b1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.local.data import android.content.Context +import android.graphics.Bitmap import android.os.StatFs import com.tomclaw.cache.DiskLruCache import dagger.hilt.android.qualifiers.ApplicationContext @@ -10,15 +11,17 @@ import kotlinx.coroutines.withContext import okio.Source import okio.buffer import okio.sink +import okio.use import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ext.compressToPNG import org.koitharu.kotatsu.core.util.ext.longHashCode +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfWriteable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -66,6 +69,16 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { } } + suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) { + val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) + try { + bitmap.compressToPNG(file) + lruCache.get().put(url, file) + } finally { + file.delete() + } + } + private suspend fun getAvailableSize(): Long = runCatchingCancellable { val statFs = StatFs(cacheDir.get().absolutePath) statFs.availableBytes diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index c8782b2ab..c7fa16972 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -1,12 +1,12 @@ package org.koitharu.kotatsu.reader.domain import android.content.Context -import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import androidx.annotation.AnyThread import androidx.collection.LongSparseArray import androidx.collection.set +import androidx.core.net.toFile import androidx.core.net.toUri import dagger.hilt.android.ActivityRetainedLifecycle import dagger.hilt.android.qualifiers.ApplicationContext @@ -25,6 +25,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit import okhttp3.OkHttpClient import okhttp3.Request +import okio.use import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.MangaHttpClient @@ -34,6 +35,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP +import org.koitharu.kotatsu.core.util.ext.compressToPNG +import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.exists import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull @@ -48,9 +51,9 @@ import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import java.io.File import java.util.LinkedList import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.ZipFile import javax.inject.Inject import kotlin.concurrent.Volatile import kotlin.coroutines.AbstractCoroutineContextElement @@ -73,6 +76,7 @@ class PageLoader @Inject constructor( private val semaphore = Semaphore(3) private val convertLock = Mutex() private val prefetchLock = Mutex() + @Volatile private var repository: MangaRepository? = null private val prefetchQueue = LinkedList() @@ -122,19 +126,30 @@ class PageLoader @Inject constructor( return loadPageAsync(page, force).await() } - suspend fun tryConvert(file: File): Boolean = convertLock.withLock { - if (context.ramAvailable < file.length() * 2) { - return@withLock false - } - runInterruptible(Dispatchers.Default) { - val image = BitmapFactory.decodeFile(file.absolutePath) - try { - file.outputStream().use { out -> - image.compress(Bitmap.CompressFormat.PNG, 100, out) + suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock { + if (uri.isZipUri()) { + val bitmap = runInterruptible(Dispatchers.IO) { + ZipFile(uri.schemeSpecificPart).use { zip -> + val entry = zip.getEntry(uri.fragment) + context.ensureRamAtLeast(entry.size * 2) + zip.getInputStream(zip.getEntry(uri.fragment)).use { + BitmapFactory.decodeStream(it) + } } + } + cache.put(uri.toString(), bitmap).toUri() + } else { + val file = uri.toFile() + context.ensureRamAtLeast(file.length() * 2) + val image = runInterruptible(Dispatchers.IO) { + BitmapFactory.decodeFile(file.absolutePath) + } + try { + image.compressToPNG(file) } finally { image.recycle() } + uri } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index b38159f88..0b58a3c31 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -1,8 +1,6 @@ package org.koitharu.kotatsu.reader.ui.pager import android.net.Uri -import androidx.core.net.toFile -import androidx.core.net.toUri import androidx.lifecycle.Observer import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener import kotlinx.coroutines.CancellationException @@ -139,13 +137,9 @@ class PageHolderDelegate( prevJob?.join() state = State.CONVERTING try { - val file = uri.toFile() - if (!loader.tryConvert(file)) { - state = State.ERROR - callback.onError(e) - } + val newUri = loader.convertBimap(uri) state = State.CONVERTED - callback.onImageReady(file.toUri()) + callback.onImageReady(newUri) } catch (ce: CancellationException) { throw ce } catch (e2: Throwable) {